Що таке каррінг у JavaScript
Каррінг (currying) - це техніка, яка перетворює функцію з кількома аргументами на ланцюг функцій з одним аргументом, де кожен виклик повертає наступну функцію, доки не зберуться всі аргументи і не виконається оригінальна логіка.
Теорія
TL;DR
- Аналогія: як замовлення кави крок за кроком - обираєш розмір (повертаються варіанти молока), потім молоко (повертаються сиропи), замість того щоб диктувати все одразу
- Кожен виклик карованої функції повертає нову функцію, яка замикається на попередньому аргументі
- Звичайна:
sum(1, 2, 3). Карована:sum(1)(2)(3). Результат однаковий, форма різна - Використовуй, коли повторно застосовуєш фіксовані аргументи (базовий URL, роль користувача, рівень логування). Для разових викликів - звичайна функція простіша
- Часткове застосування (partial application) виходить само собою: зупинись посередині, збережи результат, використовуй скрізь
Швидкий приклад
// Звичайна - всі аргументи одразу
function regularSum(a, b, c) { return a + b + c; }
regularSum(1, 2, 3); // 6
// Карована - по одному аргументу
const currySum = a => b => c => a + b + c;
currySum(1)(2)(3); // 6
// Часткове застосування: фіксуємо один аргумент
const add5 = currySum(5);
add5(3); // 8
add5(10); // 15Кожен виклик повертає функцію, яка тримає попередній аргумент у замиканні (closure). Фінальний виклик запускає логіку з усіма зібраними аргументами.
Головна різниця
Звичайна функція отримує всі аргументи одразу і або виконується, або повертає NaN/undefined якщо щось пропущено. Карована функція приймає їх по одному, тому можна зупинитись посередині, зберегти частково застосований результат і використовувати його в різних місцях коду. Цей проміжний результат і є частковим застосуванням.
Коли використовувати
- Фіксований аргумент який повторюється в багатьох викликах (базовий URL для API, роль у middleware авторизації) - закаррінгуй і створи спеціалізовані версії один раз
- Побудова пайплайнів функцій, де кожен крок очікує один вхідний аргумент
- Конфіг-важкі утиліти на кшталт логерів, де рівень встановлюється один раз і використовується скрізь
- Разова математика або проста трансформація без повторного використання - звичайна функція чистіша
Як JavaScript обробляє каррінг зсередини
V8 створює нове замикання при кожному карованому виклику. Кожне замикання зберігає попередні аргументи у лексичній області видимості - ніякого глобального стану. Коли надходить фінальний аргумент, рушій збирає повний список і викликає оригінальну функцію. Короткі ланцюги замикань V8 інлайнить. Довші можуть виділятись на купі, тому якщо каррінг відбувається у гарячому циклі - краще профілювати.
Універсальний curry-утиліт
Вручну вкладати функції одна в одну стає нудно швидко. Універсальна обгортка перевіряє скільки аргументів очікує функція (fn.length) і збирає їх поки не набере достатньо:
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const max = (a, b) => Math.max(a, b);
const curriedMax = curry(max);
curriedMax(10)(20); // 20 - по одному
curriedMax(10, 30); // 30 - обидва одразу теж працюєАргументи можна передавати по одному або групами. Базовий кейс спрацьовує як тільки загальна кількість досягає fn.length.
Типові помилки
Забутий return у зовнішній функції:
// Неправильно
function bad(a) {
function inner(b) { return a + b; } // зовнішня нічого не повертає
}
bad(1)(2); // TypeError: bad(...) is not a functionЗовнішня функція має явно повернути внутрішню. Стрілкові функції роблять цю помилку рідшою: a => b => a + b.
Припущення що JS сам каррінгує функції:
const add = (a, b) => a + b;
const add1 = add(1); // NaN - b стає undefined, а не новою функцієюНативні функції не каррінгуються самостійно. Потрібно або обгорнути curry-утилітою, або написати вручну.
Відсутній базовий кейс у власному curry:
// Неправильно - нескінченна рекурсія
function badCurry(fn) {
return function(...args) {
return badCurry(fn)(...args); // немає умови виходу, переповнення стека
};
}Завжди перевіряй args.length >= fn.length перед рекурсивним викликом.
Каррінг варіадичної функції:
const sum = (...args) => args.reduce((a, b) => a + b, 0);
const curriedSum = curry(sum); // fn.length дорівнює 0, базовий кейс одразу
curriedSum(1, 2, 3); // працює, але каррінг не має ефектуfn.length повертає 0 для rest-параметрів. Якщо потрібен каррінг - передавай очікуваний arity явно.
Де зустрічається в реальних проектах
У більшості кодових баз каррінг з'являється вперше не в теорії функціонального програмування, а в middleware-фабриці для авторизації, яку хтось написав в перший тиждень проекту і більше не змінював.
- Lodash:
_.curryобгортає будь-яку функцію для композиції -curry(map)(double)(data) - Ramda: всі функції карованi за замовчуванням, point-free стиль через
R.pipe - Express:
requireRole('admin')- найпоширеніший реальний патерн - Redux-Observable: карований
switchMapдля композиції epics - React fetch-хуки:
const getUser = curryFetch('/api/users')фіксує базовий URL один раз
Питання на співбесіді
Q: Яка різниця між каррінгом і частковим застосуванням (partial application)?
A: Часткове застосування фіксує частину аргументів і повертає функцію яка очікує решту. Каррінг завжди розбиває на кроки по одному аргументу. Будь-який каррінг підтримує часткове застосування, але не навпаки.
Q: Напиши карований варіант Array.prototype.map.
A: const curryMap = fn => arr => arr.map(fn). Використання: curryMap(x => x * 2)([1, 2, 3]) повертає [2, 4, 6].
Q: Як каррінг допомагає при композиції функцій?
A: Карована функція приймає один вхід і повертає один вихід - саме те що очікують compose і pipe. Це дозволяє будувати пайплайни на кшталт pipe(trim, toLower, greet) без функцій-обгорток навколо кожного кроку.
Q: Що станеться якщо закаррінгувати функцію з параметрами за замовчуванням?
A: Параметри за замовчуванням не враховуються в fn.length. У function add(a, b = 0) значення fn.length дорівнює 1. Універсальний curry спрацює після першого аргументу, а b завжди буде 0.
Q: Як V8 оптимізує карованi замикання на рівні рушія? (Senior)
A: V8 використовує escape analysis щоб уникнути виділення пам'яті на купі для короткоживучих замикань. Короткі ланцюги інлайняться. Гарячі шляхи що генерують багато замикань можуть деоптимізуватись - перевіряй через --trace-ic. Для критичного по продуктивності коду .bind() може бути швидшим за ручний каррінг, бо це нативна операція.
Приклади
Базовий: адер для повторного використання
const add = a => b => a + b;
const add10 = add(10); // фіксуємо 10, отримуємо адер
add10(5); // 15
add10(20); // 30
add10(-3); // 7add10 - це звичайна функція яка додає 10. Створюєш один раз, використовуєш де потрібно, не чіпаючи оригінальну add.
Середній рівень: Express middleware для авторизації за роллю
const requireRole = role => (req, res, next) => {
if (req.user?.role === role) return next();
res.status(403).send('Access denied');
};
const adminOnly = requireRole('admin');
const editorOnly = requireRole('editor');
app.get('/admin/dashboard', adminOnly, dashboardHandler);
app.get('/blog/edit', editorOnly, editHandler);
// req.user.role === 'admin' -> викликає next()
// будь-що інше -> 403Без каррінгу треба передавати роль другим аргументом у кожному роуті або дублювати логіку. З каррінгом - логіка живе в одному місці, а спеціалізовані версії просто іменуються.
Просунутий рівень: універсальний curry для логера
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) return fn(...args);
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
function formatMessage(level, timestamp, message) {
return `[${level}] ${timestamp}: ${message}`;
}
const log = curry(formatMessage);
const logError = log('ERROR'); // фіксуємо рівень
const logErrorNow = logError(new Date().toISOString()); // фіксуємо timestamp
logErrorNow('Database connection failed');
// [ERROR] 2024-01-15T10:30:00.000Z: Database connection failed
logErrorNow('Timeout on /api/users');
// [ERROR] 2024-01-15T10:30:00.000Z: Timeout on /api/users
log('INFO', new Date().toISOString(), 'Server started'); // всі одразу теж працюєБазовий кейс (args.length >= fn.length) і робить можливим передачу аргументів як по одному, так і групами.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.