Что такое каррирование и композиция функций?
Вопрос
Что такое каррирование (currying) функций и композиция (composition), зачем они нужны и как реализовать их в JavaScript?
Короткий ответ
Каррирование — это преобразование функции с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент и возвращает следующую функцию, пока не соберутся все аргументы. Композиция — это объединение нескольких функций в одну, где результат одной становится входом для следующей. Вместе эти паттерны позволяют собирать сложную логику из маленьких переиспользуемых частей в функциональном стиле.
Подробный ответ
Каррирование. Обычная функция f(a, b, c) вызывается сразу со всеми аргументами. Каррированная версия вызывается как f(a)(b)(c) — каждый вызов "запоминает" переданный аргумент через замыкание и возвращает новую функцию, ожидающую следующий. Каррирование полезно для:
- Частичного применения (partial application) — заранее зафиксировать часть аргументов и получить специализированную функцию (например,
multiply(2)→ функция "удвоить"). - Переиспользования и композиции — каррированные функции с одним аргументом легко комбинировать в цепочки/пайплайны.
- Более выразительного API — например, в конфигурируемых обработчиках событий:
addValidator(rule)(value).
Общая реализация — универсальная функция curry, которая накапливает аргументы, пока их количество не сравняется с fn.length (арностью исходной функции), и только тогда вызывает оригинальную функцию.
Композиция. Принцип «выход одной функции — вход другой»: compose(f, g, h)(x) эквивалентно f(g(h(x))) — выполнение справа налево, как в математике. pipe — тот же принцип, но слева направо (pipe(f, g, h)(x) = h(g(f(x)))), что часто читается естественнее в коде (порядок действий как в предложении). Оба паттерна реализуются через reduce по массиву функций.
Композиция и каррирование хорошо дополняют друг друга: каррированные функции с одним "содержательным" аргументом (данные) идеально подходят как звенья пайплайна композиции, потому что у каждой функции в цепочке ровно один вход и один выход.
Компромиссы. Эти паттерны дают чистоту и переиспользуемость, но:
- Усложняют отладку — трассировка стека через цепочку композиций менее наглядна, чем последовательные
constс промежуточными именами. - Требуют аккуратности с побочными эффектами — паттерн предполагает чистые функции (без мутаций и I/O внутри), иначе выгода от предсказуемости теряется.
- Могут ухудшать читаемость для команд, не привыкших к функциональному стилю — важен баланс, а не догматизм.
Пример
// Универсальная функция curry
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return (...more) => curried.apply(this, [...args, ...more])
}
}
function sum3(a, b, c) {
return a + b + c
}
const curriedSum = curry(sum3)
console.log(curriedSum(1)(2)(3)) // 6
console.log(curriedSum(1, 2)(3)) // 6 — можно передавать несколько сразу
console.log(curriedSum(1)(2, 3)) // 6
// Частичное применение через каррирование
const multiply = curry((a, b) => a * b)
const double = multiply(2)
console.log(double(5)) // 10
console.log(double(21)) // 42
// compose: справа налево
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x)
// pipe: слева направо
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x)
const trim = (s) => s.trim()
const toLowerCase = (s) => s.toLowerCase()
const removeSpaces = (s) => s.replace(/\s+/g, '-')
const slugify = pipe(trim, toLowerCase, removeSpaces)
console.log(slugify(' Hello World ')) // "hello-world"
// Композиция каррированных функций в реальном пайплайне обработки данных
const users = [
{ name: 'Alice', age: 30, active: true },
{ name: 'Bob', age: 17, active: true },
{ name: 'Eve', age: 25, active: false },
]
const filterBy = curry((predicate, arr) => arr.filter(predicate))
const mapBy = curry((mapper, arr) => arr.map(mapper))
const getActiveAdultNames = pipe(
filterBy((u) => u.active),
filterBy((u) => u.age >= 18),
mapBy((u) => u.name)
)
console.log(getActiveAdultNames(users)) // ["Alice"]
Дополнительные вопросы
- Как каррирование связано с частичным применением (
Function.prototype.bind) и чем отличается от него? - Как обобщить
curry, чтобы он корректно работал с функциями переменной арности (rest-параметрами)? - Какие трудности возникают при отладке длинных цепочек
pipe/composeи как их смягчить (например, именование промежуточных шагов)?