Каррінг просунута техніка для роботи з функціями. Вона використовується не лише в 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).
Коментарі
<code>, для кількох рядків – обгорніть їх тегом<pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)