Частичное применение и каррирование

Замыкания — основа для partial application и currying. Из функции с N аргументами получаем цепочку функций с одним аргументом, фиксируя часть аргументов в замыкании.

Идея

автор: функция — это абстракция, и уровень абстракции можно менять. Из конкретной функции (Math.log — натуральный) можно сделать абстрактную (log(n, base) = log(n) / log(base)). А потом частично применить базу и получить новую специализированную функцию.

Замыкание как механизм

// Из двух аргументов делаем функцию одного через вложенные лямбды
const createLog = (base) => (n) => Math.log(n) / Math.log(base);
const lg = createLog(10);
const ln = createLog(Math.E);

Если функция возвращена из контекста другой функции, то она будет видеть контекст верхней функции — это и называется замыкание. Мы замкнули функцию на контекст.

bind для partial

function log(base, n) { return Math.log(n) / Math.log(base); }
const lg = log.bind(null, 10);  // 10 → base, n остался
lg(1000); // log(10, 1000)

bind закрепляет this и часть аргументов слева. Возврат — новая функция с меньшей арностью.

Своя partial без bind

const partial = (fn, ...args) =>
  (...rest) => fn(...args, ...rest);

const sum = (a, b, c) => a + b + c;
const f1 = partial(sum, 10);   // a = 10
const f2 = partial(f1, 5);     // b = 5
f2(3); // sum(10, 5, 3) = 18

Определение

Частичное применение — это когда мы часть аргументов уже передали, применили, а какой-то остаток ещё торчит, и его нужно последовательно передавать.

Мы можем часть аргументов передать, а потом саму эту функцию с уже попавшими в неё аргументами передать по ссылке в другую часть программы, и никто не сможет выколупать те аргументы. — инкапсуляция через замыкание.

Curry: каждый аргумент по отдельности

const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) return fn.apply(this, args);
    return (...rest) => curried.apply(this, args.concat(rest));
  };
};

const add = curry((a, b, c) => a + b + c);
add(1)(2)(3);    // 6
add(1, 2)(3);    // 6
add(1)(2, 3);    // 6

Где это нужно

  • Создание специализированных утилит из общих
  • Унификация арности под pipe/compose (все функции принимают 1 аргумент)
  • Логгеры, валидаторы, обработчики событий с предзаполненной частью
  • Замена громоздкого DI на цепочку partial

Тонкости

  • Цепочка из 10 вложенных лямбд для функции от 10 аргументов — теоретически возможно, но неудобно
  • bind теряет .name и не оптимально работает с this в стрелочных функциях
  • В V8 каждая partial-функция = отдельный closure scope; держи руку на пульсе, если их создаются миллионы

Связанные темы

Ресурсы