Функциональное программирование на JS

В чистом FP нет оператора присвоения, нет let/var, нет if-операторов, нет циклов. Программа — это композиция чистых функций, выраженная expressions; ветвление через тернарник, итерация через рекурсию или map/reduce.

Базовые ограничения чистого FP

Свойство FP-стиль
Присвоение = это binding идентификатора, не мутация ячейки
Переменные Только const, только функциональный тип
if-операторы Нет — только тернарный оператор как expression
Циклы Нет — рекурсия или итерирующие функции
Блоки операторов Нет — справа от стрелки только expression
Запятая (comma operator) Нет — это явная последовательность

В функциональном программировании у нас могут быть объявления только функций. Внутри функции не создаём переменных.

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

Императивный vs функциональный — пример

// Императивный стиль
const pi3 = pi / 3;
const r1q = square(r1);
const r2q = square(r2);
const v = pi3 * h * (r1q + r2q + r1 * r2);

// FP-стиль — одно выражение
const volume = (h, r1, r2) =>
  (pi / 3) * h * (square(r1) + square(r2) + r1 * r2);

В чисто функциональном программировании у лямбда не может быть никаких блоков операторов с правой стороны от стрелки. Только функциональные выражения.

Порядок вычислений непредсказуем

Что будет первым вычислено — это или это выражение в скобочках — я сразу сказать не могу.

В expressions компилятор может переставлять порядок вычисления подвыражений. Comma operator (запятая) этого преимущества лишает.

Условия через тернарник

const era = (born) => (born < 0 ? 'BC' : 'AD');
const toStr = ({ name, born }) =>
  `${name}, ${Math.abs(born)} ${era(born)}`;

Цена иммутабельности

Функциональные программы требуют больше памяти для исполнения, потому что они никогда не изменяют объектов, которые обрабатывают.

Удалить элемент = создать новый массив. Зато предсказуемость и параллельность бесплатно.

Циклы заменяются рекурсией и map

Весь control flow должен идти только в одном направлении. Внутри функционального выражения ничего не может исполняться последовательно несколько раз.

// for (let i = 0; i < arr.length; i++) ...  // нельзя
const doubled = arr.map(x => x * 2);          // можно

Но стек не резиновый — глубокая рекурсия упирается в лимит. На практике в JS чистая рекурсия не tail-call optimized, поэтому реальный FP-стиль в JS почти всегда использует методы массивов под капотом с императивным for.

Деструктуризация хуже оптимизируется

Тут у нас мы сначала забираем все аргументы в массив, а потом их разворачиваем, деструктурируем... я не уверен, сможет ли оно оптимизировать это таким же образом.

V8 лучше работает с явными аргументами фиксированной арности и фиксированных типов, чем со spread/rest в сигнатуре.

Фабрика-объект как expression

const cone = (h, r1, r2, v, s) => ({ h, r1, r2, v, s });
// круглые скобки делают объектный литерал expression-ом

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

Ресурсы