Как это работает
Ручное каррирование для двух аргументов
Простейший случай — обернуть функцию от двух аргументов:
function curry2(fn) {
return function (a) {
return function (b) {
return fn(a, b)
}
}
}
const add = (x, y) => x + y
const curriedAdd = curry2(add)
curriedAdd(2)(3) // 5
Шаг за шагом:
curry2(add)возвращает(a) => (b) => add(a, b)curriedAdd(2)возвращает(b) => add(2, b)— замыкание, захватившееa = 2- Результат
(3)вычисляетadd(2, 3)— получаем5
Этот подход не масштабируется: для трёх аргументов нужна curry3, для четырёх — curry4. Нужна универсальная реализация.
fn.length — ключ к универсальному curry
Свойство fn.length возвращает количество формальных параметров функции. Именно на нём строится автоматическое каррирование.
function sum(a, b, c) { return a + b + c }
sum.length // 3
// Параметры по умолчанию НЕ считаются
function greet(name, greeting = 'Привет') { return `${greeting}, ${name}` }
greet.length // 1
// Rest-параметры НЕ считаются
function log(...args) { console.log(args) }
log.length // 0
// Считаются только параметры ДО первого значения по умолчанию
function mixed(a, b = 1, c) { return a + b + c }
mixed.length // 1 — только a
Правило: fn.length учитывает параметры до первого с = или .... Всё, что после, — невидимо.
Универсальная реализация
function curry(fn) {
// Рекурсивная функция накопления аргументов
return function curried(...args) {
// Набрали достаточно аргументов — вызываем оригинал
if (args.length >= fn.length) {
return fn(...args)
}
// Не хватает — возвращаем функцию, которая добавит недостающие
return function (...args2) {
return curried(...args.concat(args2))
}
}
}
Проверяем:
function volume(l, w, h) {
return l * w * h
}
const curriedVolume = curry(volume)
// Все варианты вызова работают
curriedVolume(2)(3)(4) // 24
curriedVolume(2, 3)(4) // 24
curriedVolume(2)(3, 4) // 24
curriedVolume(2, 3, 4) // 24
Разбор по шагам
Вызов curriedVolume(2)(3)(4):
curried(2)—args = [2],1 < 3→ возвращаем новую функцию- Новая функция
(3)→ вызываетcurried(2, 3)—args = [2, 3],2 < 3→ ещё одна функция - Новая функция
(4)→ вызываетcurried(2, 3, 4)—args = [2, 3, 4],3 >= 3→ вызываемvolume(2, 3, 4)→24
Вызов curriedVolume(2, 3)(4):
curried(2, 3)—args = [2, 3],2 < 3→ возвращаем функцию- Новая функция
(4)→curried(2, 3, 4)→volume(2, 3, 4)→24
Ключевой приём — args.concat(args2): накопление аргументов через создание нового массива. Не мутируем, а собираем заново.
Overloaded каррирование
Реализация выше уже «overloaded» — поддерживает и f(a)(b)(c), и f(a, b)(c), и f(a, b, c). Это прагматичный подход: чистое каррирование требует строго по одному аргументу, но в JavaScript удобнее разрешить оба стиля.
const add3 = curry((a, b, c) => a + b + c)
// Чистое каррирование
add3(1)(2)(3) // 6
// Overloaded: передаём несколько аргументов за раз
add3(1, 2)(3) // 6
add3(1)(2, 3) // 6
add3(1, 2, 3) // 6 — обычный вызов тоже работает
Альтернатива: curry через bind
Function.prototype.bind фиксирует аргументы слева — можно использовать для «бедного каррирования»:
function multiply(a, b) {
return a * b
}
const double = multiply.bind(null, 2)
double(5) // 10
bind не создаёт рекурсивную цепочку — это одноразовая фиксация. Для полноценного каррирования нужен curry. Но для простых случаев — вполне рабочая альтернатива.
V8 оптимизирует вложенные функции через inline caching и escape analysis. Если каррированная функция вызывается с одними и теми же «формами» аргументов, V8 создаёт оптимизированный машинный код через TurboFan. Однако каждый вызов промежуточной функции — это создание нового объекта-замыкания в куче. При интенсивном каррировании (миллионы вызовов в цикле) это создаёт давление на сборщик мусора. На практике разница незаметна — V8 справляется с типичными паттернами каррирования без проблем. Подробнее о JIT-оптимизации — в главе V8: JIT и оптимизации.
Итого
| Концепция | Описание |
|---|---|
fn.length | Количество параметров до первого = или ... |
Универсальный curry | Рекурсивное накопление аргументов, вызов при args.length >= fn.length |
| Overloaded | Поддержка и f(a)(b), и f(a, b) в одной реализации |
bind | Одноразовая фиксация аргументов — альтернатива для простых случаев |
| V8 | Оптимизирует вложенные функции, но каждый вызов создаёт замыкание в куче |