Композиция функций (pipe, compose)

compose(f, g)(x) = f(g(x)) — справа налево, как в математике. pipe — слева направо, естественнее для человека. Возвращают новую функцию, вычисления откладываются до её вызова.

Композиция vs суперпозиция

Composition: объединяем функции в одну без вычислений. Superposition: результат одной функции передаём как аргумент в другую — тут уже происходит вычисление.

Композиция — это создание новой функции. Суперпозиция — это применение.

Базовая реализация

// compose: справа налево
const compose = (f, g) => (x) => f(g(x));

// pipe: слева направо
const pipe = (f, g) => (x) => g(f(x));

const capitalize = compose(upperCapital, lower);
capitalize('MARCUS AURELIUS'); // 'Marcus Aurelius'
// сначала lower, потом upperCapital

REST для N функций

const pipe = (...fns) => (x) =>
  fns.reduce((v, f) => f(v), x);

const compose = (...fns) => (x) =>
  fns.reduceRight((v, f) => f(v), x);

// или через reverse
const compose2 = (...fns) => (x) =>
  fns.reverse().reduce((v, f) => f(v), x);

В pipe скажем так более естественное для человека применение функций — это слева направо.

Логичнее, когда все функции унарны

Каждая функция возвращает только одно значение. Значит на вход следующей функции попадёт тоже только один аргумент. Было бы логично, чтобы все они были равноценны.

Поэтому функции для pipe/compose делают унарными. Если у функции несколько аргументов — каррируй.

Откладывание вычислений

Когда pipe/compose сделали вызов и присвоили результат в capitalize, никаких вычислений ещё не произошло — мы получили новую функцию.

const transform = pipe(double, addOne, square);
// функция собрана, но ещё не вызвана
transform(5); // только сейчас вычисления

Замена операторов функциями

const inc = x => x + 1;
const add = (a, b) => a + b;
const iif = (cond, a, b) => cond ? a : b;
const loop = (cond, body) => { while (cond) body; };

Я все операторы заменил функциями. inc, add, multiply, if, даже loop. Можно написать любую программу только на объявлении и вызове функций. Но синтаксис будет не очень удобным.

Цель — продемонстрировать, что функции достаточно как единственной абстракции (Turing-complete).

Reduce под капотом

Цикл reduce импортирован из ФП, но реализован в V8 императивно через for. Декларативность — это про синтаксис на верхнем уровне, не про машину.

Декларативность — это свойство в принципе любого хорошего кода. У декларативного кода всегда под капотом будет императивный.

forEach концептуально неверен

forEach на самом деле немножечко концептуально неверен. Вместо forEach всегда можно сделать reduce, filter, map, every. На каждый случай итерирования нужно использовать разный метод.

Trim логичнее в начале pipe

Когда в pipe идут трансформации строки — trim/normalize ставят первыми, потом более специфические преобразования.

Не бери compose из npm

Никакого NPM-пакета ставить не надо. Простой compose пишется сам. Если уж берёте зависимость — берите underscore или lodash, но не безымянный compose-пакет из npm.

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

Ресурсы