Kodokon kodokon.com

Функциональные паттерны: каррирование, композиция, неизменяемость

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

9 мин · 3 вопросов

Открыть этот урок в Kodokon

JavaScript трактует функции как значения первого класса, и это открывает дорогу функциональным паттернам. Каррирование превращает функцию от n аргументов в цепочку функций от одного аргумента: f(a, b, c) становится f(a)(b)(c). Не путай его с частичным применением, которое фиксирует часть аргументов за один шаг (fn.bind(null, a)). Каррирование особенно хорошо, когда нужно создавать специализированные функции из общих.

JAVASCRIPT
const curry = (fn) => {
  const arity = fn.length;
  return function curried(...args) {
    if (args.length >= arity) return fn(...args);
    return (...rest) => curried(...args, ...rest);
  };
};

const add3 = (a, b, c) => a + b + c;
const addCurried = curry(add3);

console.log(addCurried(1)(2)(3)); // 6
console.log(addCurried(1, 2)(3)); // 6
Универсальный curry, опирающийся на арность функции.

Тонкость спецификации: fn.length считает только параметры, стоящие перед первым параметром со значением по умолчанию или rest-параметром. У (a, b = 1) => {} длина равна 1, у (...args) => {} - 0. Поэтому в curry, основанный на арности, нужно передавать функции с простыми параметрами. Композиция - следующий шаг: сборка маленьких чистых функций в читаемые конвейеры.

JAVASCRIPT
const pipe = (...fns) => (input) =>
  fns.reduce((acc, fn) => fn(acc), input);

const trim = (s) => s.trim();
const lower = (s) => s.toLowerCase();
const dashes = (s) => s.replace(/\s+/g, "-");

const toSlug = pipe(trim, lower, dashes);
console.log(toSlug("  Hello World  "));
// "hello-world"
pipe применяет функции слева направо.

pipe(f, g, h)(x) вычисляет h(g(f(x))): это читается в порядке выполнения. Вариант compose применяет функции справа налево, как в математической записи. Эти конвейеры ценны, только если функции чистые - без побочных эффектов, без скрытых зависимостей, - а это требует неизменяемых данных: вместо того чтобы менять объект, ты создаёшь его новую версию, и тогда равенства ссылок достаточно, чтобы обнаружить изменение (сердце согласования в React).

JAVASCRIPT
const state = Object.freeze({
  user: { name: "Ada" },
  tags: ["js"]
});

const next = {
  ...state,
  tags: [...state.tags, "fp"]
};

const deepCopy = structuredClone(state);
deepCopy.user.name = "Grace"; // independent copy
Новая версия через копирование, а не через мутацию.

Проверка знаний

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

  1. В чём разница между каррированием и частичным применением?
    • Никакой, это синонимы
    • Каррирование превращает f(a, b, c) в f(a)(b)(c); частичное применение фиксирует часть аргументов за один вызов
    • Частичное применение работает только с bind, каррирование - только со стрелочными функциями
    • Каррирование применимо только к функциям от двух аргументов
  2. Чему эквивалентно pipe(f, g, h)(x)?
    • f(g(h(x)))
    • h(g(f(x)))
    • g(f(h(x)))
    • f(x) + g(x) + h(x)
  3. После const s = Object.freeze({ a: { b: 1 } }) сработает ли присваивание s.a.b = 2?
    • Нет, freeze замораживает всё дерево объекта
    • Нет, TypeError будет брошен даже вне строгого режима
    • Да, потому что freeze поверхностный: вложенный объект s.a не заморожен
    • Да, но только в нестрогом режиме