Learning Book

Как это работает

Ручное каррирование для двух аргументов

Простейший случай — обернуть функцию от двух аргументов:

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

Шаг за шагом:

  1. curry2(add) возвращает (a) => (b) => add(a, b)
  2. curriedAdd(2) возвращает (b) => add(2, b) — замыкание, захватившее a = 2
  3. Результат (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):

  1. curried(2)args = [2], 1 < 3 → возвращаем новую функцию
  2. Новая функция (3) → вызывает curried(2, 3)args = [2, 3], 2 < 3 → ещё одна функция
  3. Новая функция (4) → вызывает curried(2, 3, 4)args = [2, 3, 4], 3 >= 3 → вызываем volume(2, 3, 4)24

Вызов curriedVolume(2, 3)(4):

  1. curried(2, 3)args = [2, 3], 2 < 3 → возвращаем функцию
  2. Новая функция (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Оптимизирует вложенные функции, но каждый вызов создаёт замыкание в куче