ФП: четыре столпа и переход от мутации к выражению
Мы открываем ФП-серию. Через пять уроков (этот плюс четыре следующих)
ты дойдёшь до воркшопа fp-ts-car-valuation и прочитаешь его без
шифровки. Сегодня кладём фундамент: четыре столпа функционального
программирования и одно простое движение, “не меняй, а возвращай новое”.
Цель урока узкая. После него ты отличаешь чистую функцию от грязной,
понимаешь, почему temp = 0 в середине функции это не “стиль”, а
нарушение свойства, и готов к уроку 05 про композицию.
Семь определений на десять человек
Спроси десять разработчиков, что такое функциональное программирование, получишь семь разных ответов. Спроси, отличается ли pure FP от просто FP, ответов станет восемь.
ФП живёт несколькими школами. Lisp-семья (Clojure, Scheme) ставит на макросы и работу с кодом как с данными. ML-семья (Haskell, OCaml, Scala) ставит на алгебру типов и строгие гарантии. Между ними мы видим разные варианты: где-то лень и referential transparency обязательны, где-то это рекомендация.
Чтобы не утонуть в спорах, в этой серии возьмём рабочее определение:
ФП это декларативная парадигма, в которой программа собрана из чистых детерминированных функций, без скрытого состояния и без mutation.
“Pure FP” и просто “FP” дальше используем как синонимы. Это не корабль, не важно, как мы его называем, важно, что у него под капотом. А под капотом, четыре столпа.
Столп 1. Композиция
Большая программа, как большая стена, собирается из кирпичей. Кирпичи у нас, маленькие функции. Чем чище и проще каждый кирпич, тем дальше доходит стена.
Композиция значит вот что. Берём f: A -> B и g: B -> C, получаем
g ∘ f: A -> C, новую функцию, которая делает сначала f, потом g.
В коде это пока выглядит как вложенные вызовы:
const result = transform(parse(read(input)));
Уже композиция, просто в неудобной записи. Урок 05 покажет pipe и
compose, и эту же запись мы перевернём слева направо. Сегодня важно
вот что: композиция требует, чтобы у функций не было хвостов сбоку.
Если parse втихую пишет в лог-файл, а read мутирует кеш, склейка
перестаёт быть просто склейкой, появляется неявная связь между
кирпичами.
Композиция работает только на чистых функциях. Это и есть стержень серии: ради композиции, и придётся следить за остальными тремя столпами.
Столп 2. Функция как значение first-class
Higher-order function звучит академично, но идея простая. Функция в JS это обычное значение. Её можно положить в переменную, в массив, передать в другую функцию, вернуть из функции. JS даёт это с первого дня.
const add = (a: number, b: number) => a + b;
const ops = [add, (a, b) => a - b];
const apply = (f, a, b) => f(a, b);
apply(ops[0], 1, 2); // 3
Когда функция, значение, появляются полезные паттерны. sortBy
принимает “ключ для сравнения”, retry принимает “что повторить”,
measure принимает “что замерить”. Один обобщённый каркас плюс
параметр-функция вместо десятка похожих процедур.
const by = <T, K>(key: (x: T) => K) => (a: T, b: T) =>
key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0;
const users = [{ name: 'Ada', age: 36 }, { name: 'Linus', age: 55 }];
users.sort(by((u) => u.age));
by принимает функцию-ключ и возвращает функцию-компаратор. Это и есть
HOF. Запоминай ощущение: “функция как значение, передаётся, возвращается,
складывается”. В уроке 05 это станет инструментом номер один.
Столп 3. Иммутабельность
Иммутабельность не значит “никогда ничего не меняй”. Программа должна что-то менять, иначе она бесполезна. Значит, “не меняй то, что не твоё”, и шире, “вместо мутации возвращай новое значение”.
// мутирующий вариант, плохой
function addItem(cart: Item[], item: Item) {
cart.push(item);
}
// иммутабельный вариант, хороший
function addItem(cart: readonly Item[], item: Item): Item[] {
return [...cart, item];
}
Слева функция меняет аргумент. Кто получит его дальше по дереву, тот получит уже изменённую версию, не зная об этом. Справа функция возвращает новый массив, исходный не трогает.
Trade-off честный. Новый объект на каждое изменение стоит памяти и аллокаций. Для продакшена есть персистентные структуры данных: Immer (для JS), Immutable.js, Mori, в Clojure это идёт из коробки. Идея у всех одна, structural sharing: на изменении переиспользуется большая часть старого дерева, копируется только путь до корня.
Польза иммутабельности накапливается тихо. Рассуждать о коде проще (значение, попавшее в переменную, там и остаётся). Concurrency безопаснее (нечего гонкам делать с константой). Undo/redo получается почти бесплатно (старые значения никуда не делись).
дальше после входа
Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.
Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.