Каррирование – продвинутая техника для работы с функциями. Она используется не только в JavaScript, но и в других языках.
Каррирование – это трансформация функций таким образом, чтобы они принимали аргументы не как f(a, b, c)
, а как f(a)(b)(c)
.
Каррирование не вызывает функцию. Оно просто трансформирует её.
Давайте сначала посмотрим на пример, чтобы лучше понять, о чём речь, а потом на практическое применение каррирования.
Создадим вспомогательную функцию curry(f)
, которая выполняет каррирование функции f
с двумя аргументами. Другими словами, curry(f)
для функции f(a, b)
трансформирует её в f(a)(b)
.
function
curry
(
f
)
{
// curry(f) выполняет каррирование
return
function
(
a
)
{
return
function
(
b
)
{
return
f
(
a,
b)
;
}
;
}
;
}
// использование
function
sum
(
a,
b
)
{
return
a +
b;
}
let
curriedSum =
curry
(
sum)
;
alert
(
curriedSum
(
1
)
(
2
)
)
;
// 3
Как вы видите, реализация довольна проста: это две обёртки.
- Результат
curry(func)
– обёрткаfunction(a)
. - Когда она вызывается как
sum(1)
, аргумент сохраняется в лексическом окружении и возвращается новая обёрткаfunction(b)
. - Далее уже эта обёртка вызывается с аргументом
2
и передаёт вызов к оригинальной функцииsum
.
Более продвинутые реализации каррирования, как например _.curry из библиотеки lodash, возвращают обёртку, которая позволяет запустить функцию как обычным образом, так и частично.
function
sum
(
a,
b
)
{
return
a +
b;
}
let
curriedSum =
_.
curry
(
sum)
;
// используем _.curry из lodash
alert
(
curriedSum
(
1
,
2
)
)
;
// 3, можно вызывать как обычно
alert
(
curriedSum
(
1
)
(
2
)
)
;
// 3, а можно частично
Каррирование? Зачем?
Чтобы понять пользу от каррирования, нам определённо нужен пример из реальной жизни.
Например, у нас есть функция логирования log(date, importance, message)
, которая форматирует и выводит информацию. В реальных проектах у таких функций есть много полезных возможностей, например, посылать логи по сети, здесь для простоты используем alert
:
function
log
(
date,
importance,
message
)
{
alert
(
`
[
${
date.
getHours
(
)
}
:
${
date.
getMinutes
(
)
}
] [
${
importance}
]
${
message}
`
)
;
}
А теперь давайте применим к ней каррирование!
log =
_.
curry
(
log)
;
После этого log
продолжает работать нормально:
log
(
new
Date
(
)
,
"DEBUG"
,
"some debug"
)
;
// log(a, b, c)
…Но также работает вариант с каррированием:
log
(
new
Date
(
)
)
(
"DEBUG"
)
(
"some debug"
)
;
// log(a)(b)(c)
Давайте сделаем удобную функцию для логов с текущим временем:
// logNow будет частичным применением функции log с фиксированным первым аргументом
let
logNow =
log
(
new
Date
(
)
)
;
// используем её
logNow
(
"INFO"
,
"message"
)
;
// [HH:mm] INFO message
Теперь logNow
– это log
с фиксированным первым аргументом, иначе говоря, «частично применённая» или «частичная» функция.
Мы можем пойти дальше и сделать удобную функцию для именно отладочных логов с текущим временем:
let
debugNow =
logNow
(
"DEBUG"
)
;
debugNow
(
"message"
)
;
// [HH:mm] DEBUG message
Итак:
- Мы ничего не потеряли после каррирования:
log
всё так же можно вызывать нормально. - Мы можем легко создавать частично применённые функции, как сделали для логов с текущим временем.
Продвинутая реализация каррирования
В случае, если вам интересны детали, вот «продвинутая» реализация каррирования для функций с множеством аргументов, которую мы могли бы использовать выше.
Она очень короткая:
function
curry
(
func
)
{
return
function
curried
(
...
args
)
{
if
(
args.
length >=
func.
length)
{
return
func
.
apply
(
this
,
args)
;
}
else
{
return
function
(
...
args2
)
{
return
curried
.
apply
(
this
,
args.
concat
(
args2)
)
;
}
}
}
;
}
Примеры использования:
function
sum
(
a,
b,
c
)
{
return
a +
b +
c;
}
let
curriedSum =
curry
(
sum)
;
alert
(
curriedSum
(
1
,
2
,
3
)
)
;
// 6, всё ещё можно вызывать нормально
alert
(
curriedSum
(
1
)
(
2
,
3
)
)
;
// 6, каррирование первого аргумента
alert
(
curriedSum
(
1
)
(
2
)
(
3
)
)
;
// 6, каррирование всех аргументов
Новое curry
выглядит сложновато, но на самом деле его легко понять.
Результат вызова curry(func)
– это обёртка curried
, которая выглядит так:
// func -- функция, которую мы трансформируем
function
curried
(
...
args
)
{
if
(
args.
length >=
func.
length)
{
// (1)
return
func
.
apply
(
this
,
args)
;
}
else
{
return
function
pass
(
...
args2
)
{
// (2)
return
curried
.
apply
(
this
,
args.
concat
(
args2)
)
;
}
}
}
;
Когда мы запускаем её, есть две ветви выполнения if
:
- Вызвать сейчас: если количество переданных аргументов
args
совпадает с количеством аргументов при объявлении функции (func.length
) или больше, тогда вызов просто переходит к ней. - Частичное применение: в противном случае
func
не вызывается сразу. Вместо этого, возвращается другая обёрткаpass
, которая снова применитcurried
, передав предыдущие аргументы вместе с новыми. Затем при новом вызове мы опять получим либо новое частичное применение (если аргументов недостаточно) либо, наконец, результат.
Например, давайте посмотрим, что произойдёт в случае sum(a, b, c)
. У неё три аргумента, так что sum.length = 3
.
Для вызова curried(1)(2)(3)
:
- Первый вызов
curried(1)
запоминает1
в своём лексическом окружении и возвращает обёрткуpass
. - Обёртка
pass
вызывается с(2)
: она берёт предыдущие аргументы (1
), объединяет их с тем, что получила сама(2)
и вызываетcurried(1, 2)
со всеми ними. Так как число аргументов всё ещё меньше 3-х,curry
возвращаетpass
. - Обёртка
pass
вызывается снова с(3)
. Для следующего вызоваpass(3)
берёт предыдущие аргументы (1
,2
) и добавляет к ним3
, делая вызовcurried(1, 2, 3)
– наконец 3 аргумента, и они передаются оригинальной функции.
Если всё ещё не понятно, просто распишите последовательность вызовов на бумаге.
Для каррирования необходима функция с фиксированным количеством аргументов.
Функцию, которая использует остаточные параметры, типа f(...args)
, так каррировать не получится.
По определению, каррирование должно превращать sum(a, b, c)
в sum(a)(b)(c)
.
Но, как было описано, большинство реализаций каррирования в JavaScript более продвинуты: они также оставляют вариант вызова функции с несколькими аргументами.
Итого
Каррирование – это трансформация, которая превращает вызов f(a, b, c)
в f(a)(b)(c)
. В JavaScript реализация обычно позволяет вызывать функцию обоими вариантами: либо нормально, либо возвращает частично применённую функцию, если количество аргументов недостаточно.
Каррирование позволяет легко получать частичные функции. Как мы видели в примерах с логами: универсальная функция log(date, importance, message)
после каррирования возвращает нам частично применённую функцию, когда вызывается с одним аргументом, как log(date)
или двумя аргументами, как log(date, importance)
.
Комментарии
<code>
, для нескольких строк кода — тег<pre>
, если больше 10 строк — ссылку на песочницу (plnkr, JSBin, codepen…)