Каррінг просунута техніка для роботи з функціями. Вона використовується не лише в 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(...args2) { // (2)
return curried.apply(this, args.concat(args2));
}
}
};
Коли ми запускаємо її, є дві гілки виконання if
:
- Якщо кількість переданих
args
дорівнює або більше, ніж вказано у визначенні початковій функції(func.length)
, то викликаємо її за допомогоюfunc.apply
. - Часткове застосування: інакше
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)
.