Перейти к содержимому

Currying

Curry реализуют на всех уровнях: джун — простая обёртка, сеньор — вариативный curry с поддержкой f(a)(b,c) и f(a,b)(c). Интервьюер проверяет знание Function.length, замыканий и рекурсии. Правильный ответ — накопление аргументов через fn.length с конкатенацией.

Вариативный curry через fn.length — поддерживает любое разбиение аргументов:

/**
* Каррирование произвольной функции.
* Поддерживает f(a)(b,c), f(a,b)(c), f(a,b,c), f(a)(b)(c).
* @template T
* @param {Function} fn
* @returns {Function}
*/
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
// usage
const add = curry((a, b, c) => a + b + c);
console.log(add(1)(2)(3)); // 6 — f(a)(b)(c)
console.log(add(1, 2)(3)); // 6 — f(a,b)(c)
console.log(add(1)(2, 3)); // 6 — f(a)(b,c)
console.log(add(1, 2, 3)); // 6 — f(a,b,c)
// Частичное применение
const add5 = add(5); // ждёт ещё 2 аргумента
console.log(add5(3)(10)); // 18
// Point-free стиль
const double = curry((factor, x) => factor * x)(2);
const result = [1, 2, 3].map(double); // [2, 4, 6]
// Реальный пример: настраиваемый fetch
const request = curry((method, url, data) =>
fetch(url, { method, body: JSON.stringify(data) })
);
const post = request('POST');
const getUser = post('/api/users');
getUser({ name: 'Alice' }); // POST /api/users

Расширения: curryN для rest-функций и placeholder-версия:

// fn.length не учитывает: rest-параметры, параметры со значениями по умолчанию
// function f(a, b = 1, ...rest) {} → f.length === 1 (только до первого default!)
// Для таких функций — curryN с явной арностью:
function curryN(fn, arity) {
return function curried(...args) {
if (args.length >= arity) return fn.apply(this, args);
return (...more) => curried(...args, ...more);
};
}
const sum = curryN((...xs) => xs.reduce((a, b) => a + b, 0), 3);
console.log(sum(1)(2)(3)); // 6
// ─── Placeholder-версия (продвинутый уровень) ─────────────────────────────
const _ = Symbol('placeholder');
function curryP(fn) {
const arity = fn.length;
return function curried(...args) {
const filled = args.filter(a => a !== _).length;
if (filled >= arity && !args.includes(_)) {
return fn(...args.slice(0, arity));
}
return function (...newArgs) {
// Заменяем placeholder-ы на новые аргументы слева направо
const merged = args.map(a =>
a === _ && newArgs.length ? newArgs.shift() : a
);
return curried(...merged, ...newArgs);
};
};
}
// usage с placeholder
const divide = curryP((a, b) => a / b);
const half = divide(_, 2); // _ — первый аргумент ожидается позже
console.log(half(10)); // 5
const tenDivBy = divide(10, _);
console.log(tenDivBy(2)); // 5

Сложность

  • Время: O(n) конкатенация при каждом частичном вызове, где n = накопленные args
  • Память: O(n) — все накопленные аргументы живут в замыкании до финального вызова

Итог: Curry накапливает аргументы через замыкание до fn.length, затем вызывает оригинал. curryN — для rest/default-параметров. Placeholder-версия — для пропуска позиций.