14 січня 2022 р.

Каррінг

Каррінг просунута техніка для роботи з функціями. Вона використовується не лише в 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

Отже:

  1. Ми нічого не втратили після каррінгу: log все так само можна викликати нормально.
  2. Ми можемо легко створювати частково застосовані функції, як зробили для логів з поточним часом.

Просунута реалізація каррінгу

У разі, якщо вам цікаві деталі, ось “просунута” реалізація каррінгу для функцій з множиною аргументів, яку ми могли б використати вище.

Вона дуже коротка:

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:

  1. Якщо кількість переданих args дорівнює або більше, ніж вказано у визначенні початковій функції (func.length), то викликаємо її за допомогою func.apply.
  2. Часткове застосування: інакше func не викликається відразу. Замість цього, повертається інша обгортка pass, яка знову застосує curried, передавши попередні аргументи разом з новими.

Потім при новому виклику ми знову отримаємо або нове часткове застосування (якщо аргументів недостатньо) або, нарешті, результат.

Наприклад, давайте подивимося, що станеться у разі sum(a, b, c). У неї три аргументи, так що sum.length = 3.

Для виклику curried(1)(2)(3):

  1. Перший виклик curried(1) запам’ятовує 1 у своєму лексичному середовищі і повертає обгортку pass.
  2. Обгортка pass викликається з (2): вона бере попередні аргументи (1), об’єднує їх з тим, що отримала сама (2) і викликає curried(1, 2) з усіма аргументами. Оскільки число аргументів все ще менше за 3, curry повертає pass.
  3. Обгортка 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).

Навчальна карта