Композиция функций. Функциональное программирование
2 года назад·5 мин. на чтение
Композиция — это способ построения больших модулей из более мелких. В этой статье подробно рассмотрим композицию на примере JavaScript.
Это серия статей о функциональном программировании:
Пример работы функции
Рассмотрим пример с более конкретными функциями:
Функция
Реализация
Добавим
Добавим для всех параметров, кроме
- Парадигмы программирования
- Композиция (рассматривается в этой статье)
- Функторы
- Каррирование
- Чистые функции
- Функции первого класса
Что такое композиция функций?
Композиция — это процесс объединения небольших единиц в более крупные, которые решают более крупные задачи. При композиции входные данные одной функции приходят из выходных данных предыдущей.Как работает композиция?
Математическая запись определения композиции выглядит следующим образом:В JavaScript композиция, где(f ∘ g)(x) = f(g(x))
f
и g
- это функции, будет выглядеть так:
Функцияconst compose = (f, g) => (x) => f(g(x))
compose
является композицией функций f
и g
. Выходные данные функции g
будут переданы на вход функции f
. По другому эта запись выглядела бы следующим образом:
const compose = (f, g) => { return (x) => { const gResult = g(x) const fResult = f(gResult) return fResult } }
Пример работы функции compose
Рассмотрим пример с более конкретными функциями:
Теперь составим композицию из этих маленьких функций -const getAge = (user) => user.age const isAgeAllowed = (age) => age >= 30
getAge
и isAgeAllowed
:
const user = { name: 'John', age: 35 } const isAllowedUser = compose( isAgeAllowed, getAge ) isAllowedUser(user) // true
compose
выполняет функции справа налево. Мы отправляем объект user
в функцию isAgeAllowed
. Далее user
попадает сначала в getAge
, потом результат этой функции попадает в isAgeAllowed
.
compose
и pipe
Функция pipe
очень похожа на функцию compose
. Они выполняют одну и ту же роль. Обе объединяют функции в цепочки. Однако их реализация и порядок выполнения функций отличается.
Реализация compose
и pipe
compose
реализован следующим образом:
const compose = (...fns) => { return (x) => fns.reduceRight((acc, fn) => fn(acc), x) }
pipe
реализован следующим образом:
Они отличаются лишь в применении функцииconst pipe = (...fns) => { return (x) => fns.reduce((acc, fn) => fn(acc), x) }
reduce
и reduceRight
. Это влияет лишь на порядок выполнения функций.
Порядок выполнения функций
compose
выполняет функции справа налево.
pipe
выполняет функции слева направо.
Пример
Предположим имеется 3 функции:f
, g
и h
.
При использовании compose
:
Порядок выполнения функций будет такимcompose(f, g, h) ← ← ←
h
, g
и далее f
(справа налево). И выходные данные будут передаваться в следующую функцию.
При использовании pipe
:
Порядок выполнения функций будет такимpipe(f, g, h) → → →
f
, g
и далее h
(слева направо). И выходные данные будут передаваться в следующую функцию.
Какую функцию использовать?
compose
и pipe
не сильно отличаются друг от друга. Они решают одну и ту же задачу. compose
ближе к математической нотации (f ∘ g)(x) = f(g(x))
. С pipe
визуально легче воспринимать порядок выполнения функций.
Реальный пример
В следующих примерах будем использоватьpipe
. Предположим, нужно сделать простой калькулятор цен, в котором нужно применить:
- налог (30%, по умолчанию),
- сервисный сбор (10у.е., по умолчанию),
- скидку,
- купон,
- цену доставки на основе веса.
const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { // реализация }
Реализация без композиции
Эта функция выполняет свою задачу, но ее сложно читать, тестировать и отлаживать.const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { return ( price - (price * percentCoupon) - discount + (weight * shippingPricePerKg) + serviceFees ) * (1 + taxPercentage) }
Реализация с композицией
Эта реализация выглядит более чистой, ее легко тестировать и отлаживать. Все это благодаря модульности.const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { const applyTax = (val) => val * (1 + taxPercentage) const applyServiceFees = (val) => val + serviceFees const applyPercentCoupon = (val) => val - val * percentCoupon const applyDiscount = (val) => val - discount const applyShippingCost = (val) => val + weight * shippingPricePerKg return pipe( applyPercentCoupon, applyDiscount, applyShippingCost, applyServiceFees, applyTax )(price) }
Запуск примера
Запустим, передав толькоprice
. Очевидно, получим неверный результат, т.к. передали не все обязательные параметры.
Сначала продебажим. Для дебагаpriceCalculator(10) // NaN
pipe
и compose
можно добавить удобную функцию inspect
. Она просто логирует в консоль входные данные и возвращает эти данные без изменений.
const inspect = (label) => (x) => { console.log(`${label}: ${x}`) return x }
inspect
в цепочку выполнения.
Результат будет примерно таким:const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { const applyTax = (val) => val * (1 + taxPercentage) const applyServiceFees = (val) => val + serviceFees const applyPercentCoupon = (val) => val - val * percentCoupon const applyDiscount = (val) => val - discount const applyShippingCost = (val) => val + weight * shippingPricePerKg return pipe( inspect('price'), applyPercentCoupon, inspect('after applyPercentCoupon'), applyDiscount, inspect('after applyDiscount'), applyShippingCost, inspect('after applyShippingCost'), applyServiceFees, inspect('after applyServiceFees'), applyTax )(price) }
Видим,priceCalculator(10) // price: undefined // after applyPercentCoupon: NaN // ...
price
- undefined
. Это произошло потому что price
- третий аргумент, а мы передаем его первым.
Быстрый фикс
Сделаем так, чтобы функция принимала один объект:Далее, используем:const priceCalculator = ({ taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg }) => { // ... }
Все еще получаемpriceCalculator({ price: 10 }) // price: 10 // after applyPercentCoupon: NaN // ...
NaN
, в этот раз потому что не был передан percentCoupon
и он имеет значение undefined
.
price
, значения по умолчанию.
Если запустить заново, получим результат:const priceCalculator = ({ taxPercentage = 0.3, serviceFees = 10, price, discount = 0, percentCoupon = 0, weight = 0, shippingPricePerKg = 0 }) => { // ... }
Это пример того как композиция позволяет нам тестировать и исправлять код проще и быстрее, просто проверяя области, которые вызывают подозрения. Проблема, которую мы отлаживали, была очень простой. Когда мы переходим к более масштабным модулям, все становится еще более трудным для проверки. Большие атомарные функции трудно поддерживать. Разделение функций на более мелкие упрощает отладку, тестирование, поддержку и разработку функций.priceCalculator({ price: 10 }) // 26
Для чего нужна композиция?
Композиция это объединение меньших модулей в более крупные. Если мы думаем оперируем модулями (что обеспечивается композицией), мы улучшаем:- модульное мышление,
- тестируемость,
- возможность отладки,
- поддерживаемость.
Итоги
Композиция — это способ построения больших модулей из более мелких, что делает наш код более модульным. Таким образом, проще отлаживать, тестировать, поддерживать, повторно использовать и интереснее разрабатывать функции.Чистые функции. Функциональное программирование
2 года назад·3 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Чистые функции.
Это серия статей о функциональном программировании:
- Парадигмы программирования
- Композиция
- Функторы
- Каррирование
- Чистые функции (рассматривается в этой статье)
- Функции первого класса
Что такое чистая функция?
Чистая функция — это функция, которая является детерминированной и не производит побочных эффектов.Характеристики чистой функции
️1. Чистые функции должны быть детерминированными
Детерминированная функция — это функция, которая при одном и том же входеx
всегда должна иметь один и тот же результат y
.
Примеры недетерминированных функций
Math.random
const getRandom = () => Math.random()
- Функции
Date
const getDate = () => Date.now()
getUsers
Функцияconst getUsers = await fetch('/users')
getUsers
недетерминирована, потому что пользователи могли обновиться, нет подключения к интернету, сервер может быть недоступен или что-то еще.
Комментарии к примерам
Эти примеры считаются недетерминированными, потому что для одних и тех же входных данных выходные данные будут отличаться. Детерминизм означает, что функция никогда не изменит результат при одних и тех же входных данных.️2. Чистые функции не должна иметь побочных эффектов
Побочным эффектом может быть:- Внешняя зависимость (доступ к внешним переменным, потокам ввода/вывода, чтение/запись файлов или выполнение HTTP-вызовов).
- Мутация (мутации локальных/внешних переменных или переданных аргументов по ссылке).
Примеры побочных эффектов
- Функция
isLessThanMin
Чистая функцияconst min = 60 const isLessThanMin = value => value < min
Побочный эффект заключается во внешней зависимости. Для исправления используется внедрение зависимости (dependency injection).const isLessThanMin = (min, value) => value > min
- Функция для вычисления квадратов чисел
Чистая функция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) => { user.age = 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) и улучшение производительности приложений.Улучшение опыта разработки
Принимая во внимание тот факт, что наши функции теперь детерминированы, независимы и самодостаточны. Улучшения будут очевидны.- Предсказуемость: устранение внешних факторов и изменений среды сделает функции более предсказуемыми.
- Поддерживаемость: улучшается понимание кода.
- Композиция: независимость функций и связь только через ввод и вывод, что позволит нам легко составлять композицию функций.
- Тестируемость: самодостаточность и независимость функций выведут тестируемость на новый уровень.
Улучшение производительности
- Способность к кэшированию (мемоизация): детерминизм функций даст нам возможность предсказывать, каким будет вывод для определенного ввода, затем мы можем кэшировать функции на основе вводов.
- Возможность распараллеливания: поскольку функции теперь свободны от побочных эффектов и независимы, их можно легко распараллелить.