Функциональное программирование на 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-ом
Связанные темы
Ресурсы
- Лекция: 0JxSs_GcvbQ