pure function (чистая функция)
Чистота функции – это требование к некоторым частям чистемы в Реакте. Она требуется к компонентам, редьюсерам и селекторам.
Характеристики чистых функций
- Имьютабельность (immutability –”неизменность”) – чистая функция не должна мутировать (менять) то, что в нее пришло. Например, пришли пропсы или другие параметры, то функция не должна их мутировать, иначе это будет side effect (“побочный эффект”). Проще говоря, функция должна пришедшие параметры обработать и вернуть что-то, не меняя самого “оригинала” пришедших параметров.
К примеру, некоторые методы массивов меняют оригинал (splice, reverse и др.), а другие (slice) – создают новый массив. То значит первые мы не можем применять к оригиналу.
Редьюсеры – это тоже чистые функции. На примере них видно, что если нам нужно что-то все-таки изменить, то мы для этого копию объекта. - Читсая функция обязательно имеет return, чтобы что-то возвращать. В реакт это обычно jsx.
- Чистые функции не должны иметь никаких side effects. К “побочным эффектам” относится то же изменение значений в глобальном мире, асинхронные запросы (это можно делать в методах жизненного цикла или внутри функции useEffect). По этой причине нельзя делать ajax-запросы в редьюсерах, но можно делать b[ в санках (они не чистые функции).
- Идемпотентность (Idempotence) – свойство возвращать на выходе всегда тот же самый результат при получении одних и тех же данных. Это свойство касается также и запросов (GET-запросы – идемпонетнны, а POST – нет).
2 ответа к «pure function (чистая функция)»
Функция является детерминированной, если для одного и того же набора входных значений она возвращает одинаковый результат. (с) Wiki “Идемпотентность (Idempotence) – свойство возвращать на выходе всегда тот же самый результат при получении одних и тех же данных” Всегда возникал вопрос, в чем разница ?)
Мой вариант: Детерминированность: выход функции основан на входных значениях и ничего больше: нет другого (скрытого) входа или состояния, на которое полагается функция для генерации своего выхода. Идемпотентность: функцию можно вызывать множество раз без страха, того что она выполнит непредсказуемое действие или вернет непредсказуемый результат (Например вычисление рандомного числа)
Чистые функции. Функциональное программирование
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования — Чистые функции.
- Парадигмы программирования
- Композиция
- Функторы
- Каррирование
- Чистые функции (рассматривается в этой статье)
- Функции первого класса
Что такое чистая функция?
Чистая функция — это функция, которая является детерминированной и не производит побочных эффектов.
Характеристики чистой функции
️1. Чистые функции должны быть детерминированными
Детерминированная функция — это функция, которая при одном и том же входе x всегда должна иметь один и тот же результат y .
Примеры недетерминированных функций
- Math.random
const getRandom = () => Math.random()
const getDate = () => Date.now()
const getUsers = await fetch('/users')
Функция getUsers недетерминирована, потому что пользователи могли обновиться, нет подключения к интернету, сервер может быть недоступен или что-то еще.
Комментарии к примерам
Эти примеры считаются недетерминированными, потому что для одних и тех же входных данных выходные данные будут отличаться. Детерминизм означает, что функция никогда не изменит результат при одних и тех же входных данных.
️2. Чистые функции не должна иметь побочных эффектов
- Внешняя зависимость (доступ к внешним переменным, потокам ввода/вывода, чтение/запись файлов или выполнение HTTP-вызовов).
- Мутация (мутации локальных/внешних переменных или переданных аргументов по ссылке).
Примеры побочных эффектов
- Функция isLessThanMin
const min = 60 const isLessThanMin = value => value < min
const isLessThanMin = (min, value) => value > min
Побочный эффект заключается во внешней зависимости. Для исправления используется внедрение зависимости (dependency injection).
- Функция для вычисления квадратов чисел
const squares = (nums) => < for(let i = 0; i < nums.length; i++) < nums[i] **= 2; >>
const squares = (nums) => nums.map(num => num * num)
Побочный эффект заключается в наличии императивного кода, который выполняет мутации в исходном массиве по ссылке. Для исправления используется функциональный .map , который создает новый массив.
- Функция updateUserAge
const updateUserAge = (user, age) =>const updateUserAge = (user, age) => (< . user, age >)
Побочный эффект заключается в мутации объекта user по ссылке. Нужно избегать изменения объектов по ссылке, вместо этого следует вернуть новый объект с новыми/обновленными свойствами.
- Функция getFirst2Elements
const getFirst2Elements = (arr) => arr.splice(0, 2)
const getFirst2Elements = (arr) => arr.slice(0, 2)
Побочный эффект заключается в мутировании arr , переданного по ссылке методом .splice . Для исправления используется функциональный метод .slice , который не изменяет сам массив.
Почему функции с побочными эффектами - плохо?
- Это делает функции тесно связанными с окружающей средой
- Увеличивает когнитивную нагрузку на разработчика
- Вызывает неочевидные изменения состояния
- Увеличивает кривую обучения кодовой базы разработчика
- Невозможность параллелизации
- Высокая непредсказуемость
- + потеря преимуществ чистых функций
Почему чистые функции - хорошо?
Можно вывести две основные категории улучшений. Улучшение опыта разработки (developer experience) и улучшение производительности приложений.
Улучшение опыта разработки
- Предсказуемость: устранение внешних факторов и изменений среды сделает функции более предсказуемыми.
- Поддерживаемость: улучшается понимание кода.
- Композиция: независимость функций и связь только через ввод и вывод, что позволит нам легко составлять композицию функций.
- Тестируемость: самодостаточность и независимость функций выведут тестируемость на новый уровень.
Улучшение производительности
- Способность к кэшированию (мемоизация): детерминизм функций даст нам возможность предсказывать, каким будет вывод для определенного ввода, затем мы можем кэшировать функции на основе вводов.
- Возможность распараллеливания: поскольку функции теперь свободны от побочных эффектов и независимы, их можно легко распараллелить.
Что такое функтор? Функциональное программирование
год назад · 5 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.
- Парадигмы программирования
- Композиция
- Функторы (рассматривается в этой статье)
- Каррирование
- Чистые функции
- Функции первого класса
Что такое функтор?
- обертка над значением,
- предоставляет интерфейс для преобразование (map),
- подчиняется законам функтора (поговорим о них позже).
Примеры функторов
- Массив ( Array ),
- Промис ( Promise ).
Почему массив - функтор?
- обертка над списком значений,
- предоставляет интерфейс для преобразования - метод map ,
- подчиняется законам функтора.
[1, 2, 3] // обернутое значение .map( // интерфейс для преобразования значения x => x * 2 )
Почему промис - функтор?
- обертка над любым значением из JavaSctipt типов,
- предоставляет интерфейс для преобразования - метод then ,
- подчиняется законам функтора.
const promise = new Promise((resolve, reject) => < resolve( < data: "value" >// обернутое значение, в данном случае объект ) >); promise .then( // интерфейс для преобразования значения response => console.log(response) );
Что объединяет массив (или промис) и функтор?
Функтор - это паттерн проектирования, а Array и Promise - типы данных, которые основаны на этом паттерне.
Почему мы говорим, что массив и промис - функторы?
Чтобы понять, что функторы ближе чем кажутся. Массив и промис легко понять, при это они являются мощной концепцией. Мы используем их ежедневно, даже не подозревая об их сущности.
Где использовать функторы?
Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.
Постановка задачи
Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".
Шаги алгоритма
- Найти первый продукт со скидкой,
- Применить скидку,
- Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".
Традиционное решение
const isProductWithDiscount = product => < return !isNaN(product.discount) >const findFirstDiscounted = products => < products.find(isProductWithDiscount) >const calcPriceAfterDiscount = product => < return product.price - product.discount >const findFinalPrice = (data, fallbackValue) => < if(!data || !data.products) return fallbackValue const discountedProduct = findFirstDiscounted(data.products) if(!discountedProduct) return fallbackValue return calcPriceAfterDiscount(discountedProduct) >findFinalPrice(data, "No data")
Комментарии к традиционному решению
- Атомарные логические единицы ( isProductWithDiscount , findFirstDiscounted и calcPriceAfterDiscount ),
- Логику защищена от невалидных данных.
- Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции findFinalPrice — проверка на валидность данных. Это слишком много).
- fallbackValue почти везде.
Почему нас волнуют эти улучшения?
Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.
Функтор Maybe
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.
Имплементация функтора Maybe
function Maybe(value) < const isNothing = () => < return value === null || value === undefined >const map = (fn) => < return isNothing() ? Maybe() : Maybe(fn(value)) >const getValueOrFallback = < return (fallbackValue) =>isNothing() ? fallbackValue : value; > return < map, getValueOrFallback, >; >
Пояснения к имплементации
- isNothing проверяет валидно ли обернутое в функтор Maybe значение
- map - интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению. map возвращает новое значение в другом экземпляре Maybe . Таким образом, мы можем сделать цепочку вызовов map - .map().map().map. .
- getValueOrFallback возвращает обернутое значение или запасное значение fallbackValue .
Как использовать функтор Maybe ?
С валидными данными:
Maybe('Hello') .map(x => x.substring(1)) .getValueOrFallback('fallback') // 'ello'
С невалидными данными:
Maybe(null) .map(x => x.substring(1)) // функция не будет запущена .getValueOrFallback('fallback') // 'fallback'
Функтор Maybe обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.
Решение задачи с функтором Maybe
const isProductWithDiscount = product => < return !isNaN(product.discount) >const findFirstDiscounted = products => < return products.find(isProductWithDiscount) >const calcPriceAfterDiscount = product => < return product.price - product.discount >Maybe(data) .map((x) => x.products) .map(findFirstDiscounted) .map(calcPriceAfterDiscount) .getValueOrFallback("No data")
Комментарии к решению с функтором Maybe
- мы не защищаем код сами, вместо нас это делает функтор Maybe ,
- мы указали fallbackValue только один раз.
- обертка над любым значением из JavaScript типов,
- предоставляет интерфейс для преобразования - метод map ,
- подчиняется законам функтора.
Законы функторов
Закон идентичности (Identity law)
Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.
const m1 = Maybe(value) const m2 = Maybe(value).map(v => v) // m1 и m2 эквивалентны
Закон композиции (Composition law)
Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.
const m1 = Maybe(value).map(v => f(g(v))) const m2 = Maybe(value).map(v => g(v)).map(v => f(v)) // m1 и m2 эквивалентны
Зачем использовать функторы?
- Абстракция над применением функции,
- Усиление композиции функций,
- Уменьшение количества защитного кода (как в функторе Maybe ),
- Более чистая структура кода,
- Переменные более явно указывают на то, что мы ожидаем (что Maybe моделирует значение, которое может присутствовать, а может и не присутствовать).
Что означает Абстракция над применением функции?
То, что мы передаем функцию (т.е. x => x.products ) в интерфейс преобразования (т.е. map ) обертки (т.е. Maybe ), и она знает, как позаботиться о себе (посредством своей внутренней реализации). Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку ( Array или Promise ), используя их интерфейсы преобразования ( map ). И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).
Почему функторы не используются повсеместно?
Просто потому, что мы к ним не привыкли. До .map (и .then ) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map , мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент. Функтор Maybe - лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.
Итоги
Функтор как паттерн проектирования - это простой, но очень мощный паттерн. Мы используем его ежедневно в различных типах данных, не догадываясь об этом. Было бы здорово, если мы сможем распознавать и ценить функторы немного больше и выделять им больше места в кодовой базе, потому что они делают код чище и дают нам больше возможностей.
Что такое чистые функции в JavaScript?
Чистые функции — строительные блоки в функциональном программировании. Их обожают за простоту и тестируемость.
В этой статье вы найдете чек-лист, который поможет определить чистая функция или нет.
Чек-лист
Функция должна удовлетворять двум условиям, чтобы считаться «чистой»:
— Каждый раз функция возвращает одинаковый результат, когда она вызывается с тем же набором аргументов
— Нет побочных эффектов
1. Одинаковый вход => Одинаковый выход
const add = (x, y) => x + y; add(2, 4); // 6
let x = 2; const add = (y) => < x += y; >; add(4); // x === 6 (the first time)
В первом случае значение возвращается на основании заданных параметров, независимо от того, где/когда вы его вызываете.
Если вы сложите 2 и 4, всегда получите 6.
Ничего не влияет на результат.
Нечистые функции = непостоянные результаты
Второй пример ничего не возвращает. Он полагается на общее состояние для выполнения своей работы путем увеличения переменной за пределами своей области.
Эта модель кошмар для разработчиков.
Разделяемое состояние вводит зависимость от времени. Вы получаете разные результаты в зависимости от того, когда вы вызвали функцию. В первый раз результат 6, в следующий раз 10 и так далее.
В каком случае вы получите меньше багов, которые появляются только при определенных условиях?
В каком случае с большей вероятностью вы преуспеете в многопоточной среде, где временные зависимости могут сломать систему?
Определенно в первом.
2. Нет побочных эффектов
Этот тест сам по себе контрольный список.
Примеры побочных эффектов:
- Видоизменение входных параметров
- console.log
- HTTP вызовы (AJAX/fetch)
- Изменение в файловой системе
- Запросы DOM
Советую посмотреть видео Боба Мартина.
Вот “нечистая” функция с побочным эффектом.
const impureDouble = (x) => < console.log('doubling', x); return x * 2; >; const result = impureDouble(4); console.log(< result >);
console.log здесь это побочный эффект, но он не повредит. Мы все равно получим те же результаты, учитывая те же данные.
Однако, это может вызвать проблемы.
“Нечистое” изменение объекта
const impureAssoc = (key, value, object) => < object[key] = value; >; const person = < name: 'Bobo' >; const result = impureAssoc('shoeSize', 400, person); console.log(< person, result >);
Переменная person была изменена навсегда, потому что функция была объявлена через оператор присваивания.
Разделяемое состояние означает, что влияние impureAssoc уже не полностью очевидно. Понимание влияния на систему теперь включает отслеживание каждой переменной, к которой когда-либо прикасалась, и знание ее истории.
Разделяемое состояние = временные зависимости.
Мы можем очистить impureAssoc, просто вернув новый объект с желаемыми свойствами.
“Очищаем это”
const pureAssoc = (key, value, object) => (< . object, [key]: value >); const person = < name: 'Bobo' >; const result = pureAssoc('shoeSize', 400, person); console.log(< person, result >);
Теперь pureAssoc возвращает тестируемый результат, и можно не беспокоиться, если он изменится где-то в другом месте.
Можно было сделать и так:
const pureAssoc = (key, value, object) => < const newObject = < . object >; newObject[key] = value; return newObject; >; const person = < name: 'Bobo' >; const result = pureAssoc('shoeSize', 400, person); console.log(< person, result >);
Изменять входные данные может быть опасно, но изменять их копию не проблема. Конечный результат — тестируемая, предсказуемая функция, которая работает независимо от того, где и когда вы ее вызываете.
Изменения ограничиваются этой небольшой областью, и вы все еще возвращаете значение.
Резюме
- Функция чистая, если не имеет побочных эффектов и каждый раз возвращает одинаковый результат, когда она вызывается с тем же набором аргументов.
- Побочные эффекты включают: меняющийся вход, HTTP-вызовы, запись на диск, вывод на экран.
- Вы можете безопасно клонировать, а затем менять входные параметры. Просто оставьте оригинал без изменений.
- Синтаксис распространения (… syntax) — это самый простой способ клонирования объектов и массивов.
Чистые функции VS. Нечистые функции в JavaScript
Чистые функции всегда возвращают один и тот же результат, если одни и те же аргументы передаются. Он не зависит от какого-либо состояния или данных, которые изменяются во время выполнения программы. Он должен зависеть только от входных аргументов. У них нет побочных эффектов, таких как вызовы сети или базы данных, и они не изменяют аргументы, которые им передаются.
Пример
function getSquare(x)
Нечистые функции
Любая функция, которая изменяет внутреннее состояние одного из своих аргументов или значение некоторой внешней переменной, является нечистой функцией. Они могут иметь любые побочные эффекты, такие как вызовы сети или базы данных, и могут изменять аргументы, которые передаются им.
Пример
function getSquare(items) < var len = items.length; for (var i = 0; i < len; i++) < items[i] = items[i] * items[i]; >return items; >
Math.random() - нечистая функция; он изменяет внутреннее состояние объекта Math, поэтому вы получаете разные значения при последовательных вызовах.