Хабрахабр

[Перевод] Каррирование функций в JavaScript

Функциональное программирование — это такой стиль разработки программ, в котором широко используются некоторые специфические возможности по работе с функциями. Речь идёт, в частности, о передаче функций другим функциям в качестве аргументов и о возврате функций из других функций. К функциональному стилю программирования относится и понятие «чистые функции». Выходные данные чистых функций зависят только от входных, они, при выполнении, не влияют на состояние программы.

Среди них можно отметить 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.

Эта функция, в свою очередь, вызывает исходную функцию fn, передавай ей параметры ...args и ..._arg (с использованием оператора spread). Эта функция возвращает другую функцию, которая так же принимает переменное число параметров (_arg). Функция выполняет вычисления и возвращает результат.

Применим эту функцию для создания варианта уже знакомой вам функции 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 являются объектами первого класса (их можно передавать в качестве аргументов другим функциям, возвращать из них, присваивать переменным).

Уважаемые читатели! Пользуетесь ли вы техниками каррирования и частичного применения функций в своих проектах?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть