Каррирование в JavaScript: как и зачем использовать

Что такое каррирование
Каррирование (currying) названо в честь математика Хаскелла Б. Карри и восходит к лямбда-исчислению. Идея простая: взять функцию, которая ожидает несколько аргументов, и представить её как последовательность унарных (по одному параметру) функций. Каждая вложенная функция принимает один аргумент и возвращает либо значение, либо следующую функцию, пока не будут переданы все аргументы.
Определение в одну строку: каррирование — это преобразование f(a, b, c) → f(a)(b)(c).
Важно: каррированная функция — это не то же самое, что функция с частично применёнными аргументами (хотя эти понятия часто используются вместе). Частичное применение — это результат вызова каррированной функции с некоторыми фиксированными аргументами.
Простой пример каррирования
Ниже — базовый пример на обычных функциях:
function buildSandwich(ingredient1) {
return (ingredient2) => {
return (ingredient3) => {
return `${ingredient1},${ingredient2},${ingredient3}`
}
}
} Функция buildSandwich возвращает вложенные анонимные функции. Каждая принимает один аргумент и возвращает либо следующую функцию, либо итоговую строку.
Если вызвать buildSandwich(“Bacon”), вы получите функцию (частично применённую):
console.log(buildSandwich("Bacon"))
Чтобы завершить вызов, передайте все аргументы:
buildSandwich("Bacon")("Lettuce")("Tomato")Альтернативно, используя стрелочные функции, запись компактнее:
const buildMeal = ingred1 => ingred2 => ingred3 =>
`${ingred1}, ${ingred2}. ${ingred3}`;
buildMeal("Bacon")("Lettuce")("Tomato");Стрелочные функции упрощают синтаксис и делают цепочки более читабельными.
Частично применённые функции
Частичное применение означает фиксирование части аргументов функции, чтобы получить новую функцию с меньшей арностью. Это одна из ключевых практик при использовании каррирования.
Пример простого умножения:
const multiply = (x, y) => x * y;
const curriedMultiply = x => y => x * y;
const timesTen = curriedMultiply(10);
console.log(timesTen(8)); // 80timesTen — частично применённая функция: она «захватила» x = 10 и ждёт лишь y.
Реальный пример для DOM-обновления:
const updateElemText = id => content =>
document.querySelector(`#${id}`).textContent = content;
const updateHeaderText = updateElemText('header');
updateHeaderText("Hello World!");Такой подход удобен, когда одно и то же действие (обновление элемента, логирование, запрос к API) применяется к разным данным, но с одинаковым «контекстом».
Композиция функций через каррирование
Каррирование хорошо сочетается с композицией: вы будто наслаиваете простые преобразования одно за другим. Например, в e‑commerce можно создать цепочку действий:
const addCustomer = fn => (...args) => {
console.log("Saving customer info");
return fn(...args);
}
const processOrder = fn => (...args) => {
console.log(`processing order #${args[0]}`);
return fn(...args);
}
let completeOrder = (...args) => {
console.log(`Order #${[...args].toString()} completed.`);
}
completeOrder = (processOrder(completeOrder));
completeOrder = (addCustomer(completeOrder));
completeOrder("1000");
Идея: оборачивать основную функцию декораторами, добавляя поведение (логирование, валидация, транзакции) в нужном порядке.
Универсальная функция curry — как превратить любую функцию в каррированную
Если вы используете каррирование часто, полезно иметь хелпер, который превращает обычную функцию в каррированную. Простейшая реализация:
const curry = (fn) => {
return curried = (...args) => {
if (fn.length !== args.length) {
return curried.bind(null, ...args)
}
return fn(...args);
}
}Пояснение:
- fn.length — количество формальных параметров функции fn (арность).
- Если передано меньше аргументов, чем ожидает fn, возвращается связанная (bound) версия curried с накопленными аргументами.
- Когда аргументов достаточно — вызывается исходная функция.
Пример:
const total = (x, y, z) => x + y + z;
const curriedTotal = curry(total);
console.log(curriedTotal(10)(20)(30)); // 60Важно: реализация на основе fn.length не работает ожидаемо для функций с rest-параметрами или с неявной арностью (например, когда функция получает переменное число аргументов). Для таких случаев нужен другой подход.
Когда каррирование не подходит — контрпример
- Функция с динамическим числом аргументов (rest/variadic) или с опциональными параметрами: fn.length не отражает реальную логику, поэтому простая curry-реализация ошибочна.
- Простые одноразовые утилиты: каррирование может усложнить код, если оно не приносит явных преимуществ.
- Производительность: глубокие цепочки функций создают более длинный call stack и дополнительные замыкания; в критичных по производительности местах это важно учитывать.
Пример, где каррирование бессмысленно:
// Функция, которая по сути просто возвращает массив всех аргументов
const gather = (...items) => items;
// Каррирование тут не даёт пользы: вызвать её каррированно неудобноАльтернативы и готовые инструменты
- Function.prototype.bind — можно частично фиксировать параметры через bind: const timesTen = multiply.bind(null, 10).
- Библиотеки: lodash/fp и ramda предоставляют удобные curry/partial/composition-хелперы (_.curry, R.curry).
- Частичное применение (partial) vs каррирование: partial фиксирует аргументы не обязательно слева, каррирование зависит от порядка и унарности.
Практическое руководство: когда и как вводить каррирование в кодовую базу
Короткая методология:
- Определите повторяющиеся операции с повторяющимся контекстом (например, обновление пяти разных элементов с одинаковой логикой).
- Реализуйте чистую функцию с явной арностью.
- Добавьте curry-хелпер или используйте библиотеку.
- Напишите тесты для частично применённых функций и для сценариев композиции.
- При ревью кода оцените читаемость: не заменяет ли каррирование простые и очевидные вызовы.
Критерии приёмки
- Функция корректно возвращает частично применённую функцию при передаче части аргументов.
- Поведение соответствует для всех допустимых комбинаций аргументов.
- Нет регрессий по производительности в горячих путях (профайлинг при необходимости).
Роль‑ориентированные чеклисты
Для разработчика:
- Есть ли повторяющаяся логика, которую можно вынести в частично применённую функцию?
- Написаны ли тесты для частично применённых вариантов?
- Документированы ли ожидания по арности функции?
Для ревьюера:
- Понятно ли имя возвращаемой функции и её назначение?
- Не ухудшила ли каррированная версия локальную читабельность?
- Нельзя ли заменить на более простую реализацию через bind или вспомогательную функцию?
Тесты и критерии приёмки
Примеры тест-кейсов (юнит‑тесты):
- Вызов curriedTotal(1)(2)(3) возвращает 6.
- Частично применённая функция timesTen(5) возвращает 50.
- При вызове с недостаточным числом аргументов возвращается функция, готовая принять оставшиеся аргументы.
- Функция с rest-параметрами не должна использоваться с простым curry-алгоритмом (ожидается выброс ошибки или документированное поведение).
Советы по отладке и поддержке
- Пишите короткие имена и документацию: f => g => h легко читать, пока имена отражают смысл.
- Используйте логирование внутри декораторов (addCustomer/processOrder) осторожно — это может загрязнить консоль.
- Для сложных цепочек можно давать промежуточные имена функциям, чтобы стек вызовов в ошибках оставался информативным.
Важно: каррирование — это инструмент. Оно улучшает читабельность и повторное использование, но не является обязательной парадигмой для каждого проекта.
Совместимость и миграция
- Каррирование — это уровень абстракции в коде: оно не зависит от версии JavaScript, если используется синтаксис, поддерживаемый целевой средой (стрелочные функции, bind и т.д.).
- Для старых сред (ES5) варианты с Function.prototype.bind работают лучше.
- При миграции из процедурного стиля постепенно выносите повторяемую логику в чистые функции, затем применяйте curry там, где это оправдано.
Ментальные модели и эвристики
- Думайте о каррировании как о «фиксировании контекста» — вы создаёте специализированную вариацию общей функции.
- Если вы можете правильно назвать частично применённую функцию (например, createUserWithDefaults), это хороший индикатор того, что каррирование улучшит код.
- Если название частичной функции становится бессмысленным или слишком длинным —, возможно, каррирование усложняет код.
Мини‑шпаргалка (cheat sheet)
- curry(fn): превращает функцию типа (a,b,c) в a => b => c => result.
- partial(fn, a): фиксирует первый аргумент через bind.
- compose(f, g)(x) = f(g(x)) — полезно вместе с каррированными функциями.
Пример compose с каррированными функциями:
const compose = (f, g) => (...args) => f(g(...args));
const double = x => x * 2;
const square = x => x * x;
const doubleThenSquare = compose(square, double);
console.log(doubleThenSquare(3)); // (3*2)^2 = 36Диаграмма принятия решения
flowchart TD
A[Нужно ли переиспользование контекста?] -->|Да| B{Функция имеет фиксированную арность?}
A -->|Нет| C[Не использовать каррирование]
B -->|Да| D[Использовать каррирование или partial]
B -->|Нет| E[Рассмотреть альтернативы: bind, объект настроек]
D --> F[Добавить тесты для частично применённых функций]
E --> FКраткий глоссарий
- Арность: количество формальных параметров функции.
- Частичное применение: создание функции с некоторыми фиксированными аргументами.
- Замыкание: функция помнит окружение, где была создана.
Итог и рекомендации
Каррирование — мощная техника функционального стиля программирования, которая делает код модульным и удобным для композиции. Применяйте его там, где:
- есть ясный повторяющийся контекст;
- хочется получить специализированные функции из более общей;
- и где читаемость выигрывает от явного разделения аргументов.
Не применяйте каррирование механически: для простых или динамических функций оно может добавить лишнюю сложность.
Ключевые шаги внедрения:
- Выделите чистые функции.
- Решите, какая арность имеет значение.
- Используйте curry-хелпер или библиотеку.
- Напишите тесты и добавьте документацию.
Спасибо за чтение — начните с небольшого примера в своём проекте и оцените преимущества для читаемости и повторного использования.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone