Функциональное программирование — это такой стиль разработки программ, в котором широко используются некоторые специфические возможности по работе с функциями. Речь идёт, в частности, о передаче функций другим функциям в качестве аргументов и о возврате функций из других функций. К функциональному стилю программирования относится и понятие «чистые функции». Выходные данные чистых функций зависят только от входных, они, при выполнении, не влияют на состояние программы.
Принципы функционального программирования поддерживает множество языков. Среди них можно отметить JavaScript, Haskell, Clojure, Erlang. Использование механизмов функционального программирование подразумевает знание, кроме прочих, таких концепций, как чистые функции, каррирование функций, функции высшего порядка.
Материал, перевод которого мы сегодня публикуем, посвящён каррированию. Мы поговорим о том, как работает каррирование, и о том, как знание этого механизма может пригодиться JS-разработчику.
Что такое каррирование?
Каррирование или карринг (currying) в функциональном программирование — это преобразование функции с множеством аргументов в набор вложенных функций с одним аргументом. При вызове каррированной функции с передачей ей одного аргумента, она возвращает новую функцию, которая ожидает поступления следующего аргумента. Новые функции, ожидающие следующего аргумента, возвращаются при каждом вызове каррированной функции — до тех пор, пока функция не получит все необходимые ей аргументы. Ранее полученные аргументы, благодаря механизму замыканий, ждут того момента, когда функция получит всё, что ей нужно для выполнения вычислений. После получения последнего аргумента функция выполняет вычисления и возвращает результат.
Говоря о каррировании, можно сказать, что это процесс превращения функции с несколькими аргументами в функцию с меньшей арностью.
Арность — это количество аргументов функции. Например — вот объявление пары функций:
function fn(a, b) {
//...
}
function _fn(a, b, c) {
//...
}
Функция fn
принимает два аргумента (это бинарная или 2-арная функция), функция _fn
принимает три аргумента (тернарная, 3-арная функция).
Поговорим о ситуации, когда, в ходе каррирования, функция с несколькими аргументами преобразуется в набор функций, каждая из которых принимает один аргумент.
Рассмотрим пример. У нас имеется следующая функция:
function multiply(a, b, c) {
return a * b * c;
}
Она принимает три аргумента и возвращает их произведение:
multiply(1,2,3); // 6
Теперь подумаем о том, как преобразовать её к набору функций, каждая из которых принимает один аргумент. Создадим каррированный вариант этой функции и посмотрим на то, как получить тот же результат в ходе вызова нескольких функций:
function multiply(a) {
return (b) => {
return (c) => {
return a * b * c
}
}
}
log(multiply(1)(2)(3)) // 6
Как видите, здесь мы преобразовали вызов единственной функции с тремя аргументами — multiply(1,2,3)
к вызову трёх функций — multiply(1)(2)(3)
.
Оказывается, что одна функция превратилась в несколько функций. При использовании новой конструкции каждая функция, кроме последней, возвращающей результат вычислений, принимает аргумент и возвращает другую функцию, также способную принять аргумент и возвратить другую функцию. Если конструкция вида multiply(1)(2)(3)
кажется вам не слишком понятной, давайте, чтобы лучше в этом разобраться, распишем её в таком виде:
const mul1 = multiply(1);
const mul2 = mul1(2);
const result = mul2(3);
log(result); // 6
Теперь построчно разберём то, что здесь происходит.
Сначала мы передаём аргумент 1
функции multiply
:
const mul1 = multiply(1);
При работе этой функции срабатывает такая конструкция:
return (b) => {
return (c) => {
return a * b * c
}
}
Теперь в mul1
имеется ссылка на функцию, принимающую аргумент b
. Вызовем функцию mul1
, передав ей 2
:
const mul2 = mul1(2);
В результате этого вызова выполнится следующий код:
return (c) => {
return a * b * c
}
Константа mul2
будет содержать ссылку на функцию, которая могла бы оказаться в ней, например, в результате выполнения следующей операции:
mul2 = (c) => {
return a * b * c
}
Если теперь вызвать функцию mul2
, передав ей 3
, то функция выполнит необходимые вычисления, воспользовавшись аргументами a
и b
:
const result = mul2(3);
Результатом этих вычислений будет 6
:
log(result); // 6
Функция mul2
, обладающая самым большим уровнем вложенности, имеет доступ к областям видимости, к замыканиям, формируемым функциями multiply
и mul1
. Именно поэтому в функции mul2
можно производить вычисления с переменными, объявленными в функциях, выполнение которых уже завершено, которые уже возвратили некие значения и обработаны сборщиком мусора.
Выше мы рассмотрели абстрактный пример, а вот, в сущности, такая же функция, которая предназначена для вычисления объёма прямоугольного параллелепипеда.
function volume(l,w,h) {
return l * w * h;
}
const vol = volume(100,20,90) // 180000
Вот как выглядит её каррированный вариант:
function volume(l) {
return (w) => {
return (h) => {
return l * w * h
}
}
}
const vol = volume(100)(20)(90) // 180000
Итак, каррирование базируется на следующей идее: на основе некоей функции создают другую функцию, которая возвращает специализированную функцию.
Каррирование и частичное применение функций
Сейчас, возможно, возникает ощущение, что количество вложенных функций, при представлении функции в виде набора вложенных функций, зависит от количества аргументов функции. И, если речь идёт о каррировании, то это так.
Особый вариант функции для вычисления объёма, которую мы уже видели, можно сделать и таким:
function volume(l) {
return (w, h) => {
return l * w * h
}
}
Здесь применены идеи, очень похожие на рассмотренные выше. Пользоваться этой функцией можно так:
const hV = volume(70);
hV(203,142);
hV(220,122);
hV(120,123);
А можно и так:
volume(70)(90,30);
volume(70)(390,320);
volume(70)(940,340);
Фактически, здесь можно видеть как мы, командой volume(70)
, создали специализированную функцию для вычисления объёма тел, одно из измерений которых (а именно — длина, l
), зафиксировано. Функция volume
ожидает 3 аргумента и содержит 2 вложенных функции, в отличие от предыдущей версии подобной функции, каррированный вариант которой содержал 3 вложенных функции.
Та функция, которая получилась после вызова volume(70)
реализует концепцию частичного применения функции (partial function application). Каррирование и частичное применение функций очень похожи друг на друга, но концепции это разные.
При частичном применении функцию преобразуют в другую функцию, обладающую меньшим числом аргументов (меньшей арностью). Некоторые аргументы такой функции оказываются зафиксированными (для них задаются значения по умолчанию).
Например, имеется такая функция:
function acidityRatio(x, y, z) {
return performOp(x,y,z)
}
Её можно преобразовать в такую:
function acidityRatio(x) {
return (y,z) => {
return performOp(x,y,z)
}
}
Реализация функции performOp()
здесь не приводится, так как она на рассматриваемые концепции не влияет.
Функцию, которую можно получить, вызвав новую функцию acidityRatio()
с аргументом, значение которого нужно зафиксировать, представляет собой исходную функцию, один из аргументов которой зафиксирован, а сама эта функция принимает на один аргумент меньше, чем исходная.
Каррированный вариант функции будет выглядеть так:
function acidityRatio(x) {
return (y) = > {
return (z) = > {
return performOp(x,y,z)
}
}
}
Как видите, при каррировании число вложенных функций равно числу аргументов исходной функции. Каждая из этих функций ожидает собственный аргумент. При этом понятно, что если функция аргументов не принимает, или принимает лишь один аргумент, то каррировать её нельзя.
В ситуации, когда функция имеет два аргумента, результаты её каррирования и частичного применения, можно сказать, совпадают. Например, у нас имеется такая функция:
function div(x,y) {
return x/y;
}
Предположим, нам нужно переписать её так, чтобы можно было, зафиксировав первый аргумент, получить функцию, выполняющую вычисления при передаче ей лишь второго аргумента, то есть, нам нужно частично применить эту функцию. Выглядеть это будет так:
function div(x) {
return (y) => {
return x/y;
}
}
Точно так же будет выглядеть и результат её каррирования.
О практическом применении концепций каррирования и частичного применения функций
Каррирование и частичное применение функций может оказаться полезным в различных ситуациях. Например — при разработке небольших модулей, подходящих для повторного использования.
Частичное применение функций позволяет облегчить использование универсальных модулей. Например, у нас есть интернет-магазин, в коде которого имеется функция, которая используется для вычисления суммы к оплате с учётом скидки.
function discount(price, discount) {
return price * discount
}
Есть определённая категория клиентов, назовём их «любимыми клиентами», которой мы даём скидку в 10%. Например, если такой клиент покупает что-то на $500, мы даём ему скидку размером $50:
const price = discount(500,0.10); // $50
// $500 - $50 = $450
Несложно заметить, что нам, при таком подходе, постоянно придётся вызывать эту функцию с двумя аргументами:
const price = discount(1500,0.10); // $150
// $1,500 - $150 = $1,350
const price = discount(2000,0.10); // $200
// $2,000 - $200 = $1,800
const price = discount(50,0.10); // $5
// $50 - $5 = $45
const price = discount(5000,0.10); // $500
// $5,000 - $500 = $4,500
const price = discount(300,0.10); // $30
// $300 - $30 = $270
Исходную функцию можно привести к такому виду, который позволял бы получать новые функции с заранее заданным уровнем скидки, при вызове которых им достаточно передавать сумму покупки. Функция discount()
в нашем примере имеет два аргумента. Вот как выглядит то, во что мы её преобразуем:
function discount(discount) {
return (price) => {
return price * discount;
}
}
const tenPercentDiscount = discount(0.1);
Функция tenPercentDiscount()
представляет собой результат частичного применения функции discount()
. При вызове tenPercentDiscount()
этой функции достаточно передать цену, а скидка в 10%, то есть — аргумент discount
, уже будет задана:
tenPercentDiscount(500); // $50
// $500 - $50 = $450
Если в нашем магазине имеются покупатели, которым решено дать скидку размером в 20%, то получить соответствующую функцию для работы с ними можно так:
const twentyPercentDiscount = discount(0.2);
Теперь функцию twentyPercentDiscount()
можно вызывать для расчёта стоимости товаров с учётом скидки в 20%:
twentyPercentDiscount(500); // 100
// $500 - $100 = $400
twentyPercentDiscount(5000); // 1000
// $5,000 - $1,000 = $4,000
twentyPercentDiscount(1000000); // 200000
// $1,000,000 - $200,000 = $600,000
Универсальная функция для частичного применения других функций
Разработаем функцию, которая принимает любую функцию и возвращает её вариант, представляющий собой функцию, некоторые аргументы которой уже заданы. Вот код, который позволяет это сделать (вы, если зададитесь целью разработать подобную функцию, вполне возможно, получите в результате что-то другое):
function partial(fn, ...args) {
return (..._arg) => {
return fn(...args, ..._arg);
}
}
Функция partial()
принимает функцию fn
, которую мы хотим преобразовать в частично применённую функцию, и переменное число параметров (...args
). Оператор rest
используется для того, чтобы поместить все параметры, идущие после fn
, в args
.
Эта функция возвращает другую функцию, которая так же принимает переменное число параметров (_arg
). Эта функция, в свою очередь, вызывает исходную функцию fn
, передавай ей параметры ...args
и ..._arg
(с использованием оператора spread
). Функция выполняет вычисления и возвращает результат.
Применим эту функцию для создания варианта уже знакомой вам функции volume
, предназначенной для расчёта объёма прямоугольных параллелепипедов, одна из сторон которых зафиксирована:
function volume(l,h,w) {
return l * h * w
}
const hV = partial(volume,100);
hV(200,900); // 18000000
hV(70,60); // 420000
Здесь можно найти пример универсальной функции для каррирования других функций.
Итоги
В этом материале мы поговорили о каррировании и частичном применении функций. Эти методы преобразования функций реализуются в JavaScript благодаря замыканиям и благодаря тому, что функции в JS являются объектами первого класса (их можно передавать в качестве аргументов другим функциям, возвращать из них, присваивать переменным).
Уважаемые читатели! Пользуетесь ли вы техниками каррирования и частичного применения функций в своих проектах?
Автор: ru_vds