Как реализовать функцию каррирования с использованием замыканий в JavaScript
Что проверяют
Понимает ли кандидат, что каррирование это преобразование функции от N аргументов в цепочку вызовов, и умеет ли он на замыкании накапливать уже переданные аргументы. Отдельно смотрят, знает ли он про fn.length и его ограничения.
Как отвечать
Каррирование превращает f(a, b, c) в f(a)(b)(c). Универсальная реализация накапливает аргументы в замыкании и вызывает исходную функцию, когда их набралось достаточно.
function curry(fn, arity = fn.length) {
return function curried(...args) {
if (args.length >= arity) {
return fn.apply(this, args);
}
return function (...rest) {
return curried.apply(this, [...args, ...rest]);
};
};
}
const add3 = (a, b, c) => a + b + c;
const curried = curry(add3);
curried(1)(2)(3); // 6
curried(1, 2)(3); // 6
curried(1)(2, 3); // 6
curried(1, 2, 3); // 6Ключевые детали:
fn.lengthэто счётчик обязательных аргументов, и он врёт в трёх случаях: rest-параметр не считается вообще ((a, ...rest) => {}даётlength === 1), параметр со значением по умолчанию обнуляет счёт начиная с себя ((a, b = 1, c) => {}даёт1), деструктурирующий параметр считается за один. Поэтому в сигнатуре есть явныйarity: для функций с дефолтами каррировать надоcurry(fn, 3).- Обычные
functionвместо стрелок в телеcurryнужны, чтобыthisдоезжал доfn:curried.apply(this, ...)сохраняет контекст, если каррированную функцию положили в объект как метод. args.length >= arity, а не===. Приcurried(1, 2, 3, 4)лишние аргументы просто уедут вfn, как при обычном вызове.- Каррирование это не частичное применение.
fn.bind(null, 1, 2)фиксирует часть аргументов один раз и возвращает функцию, ожидающую остальные; каррированная функция готова принимать их по одному сколько угодно раз.
Практический смысл: из общей функции дешёво получаются специализированные (const inc = curried(1)(0)), а однoаргументные функции удобно складывать в pipe/compose. В ramda и lodash/fp каррирование по умолчанию именно поэтому.
Полезное расширение, если спросят: поддержать плейсхолдеры (curried(1, _, 3)), проверяя аргументы на специальный символ перед подсчётом длины.
Где спотыкаются
Полагаются на fn.length и удивляются, что каррирование функции с дефолтным параметром зовёт её слишком рано. Второй частый провал: вызов без аргументов curried() не двигает счётчик, и цепочка возвращает новую функцию бесконечно, если явно не запретить пустой вызов.