Skip to main content

Що таке каррінг у JavaScript

Каррінг (currying) - це техніка, яка перетворює функцію з кількома аргументами на ланцюг функцій з одним аргументом, де кожен виклик повертає наступну функцію, доки не зберуться всі аргументи і не виконається оригінальна логіка.

Теорія

TL;DR

  • Аналогія: як замовлення кави крок за кроком - обираєш розмір (повертаються варіанти молока), потім молоко (повертаються сиропи), замість того щоб диктувати все одразу
  • Кожен виклик карованої функції повертає нову функцію, яка замикається на попередньому аргументі
  • Звичайна: sum(1, 2, 3). Карована: sum(1)(2)(3). Результат однаковий, форма різна
  • Використовуй, коли повторно застосовуєш фіксовані аргументи (базовий URL, роль користувача, рівень логування). Для разових викликів - звичайна функція простіша
  • Часткове застосування (partial application) виходить само собою: зупинись посередині, збережи результат, використовуй скрізь

Швидкий приклад

javascript
// Звичайна - всі аргументи одразу 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) і збирає їх поки не набере достатньо:

javascript
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 у зовнішній функції:

javascript
// Неправильно function bad(a) { function inner(b) { return a + b; } // зовнішня нічого не повертає } bad(1)(2); // TypeError: bad(...) is not a function

Зовнішня функція має явно повернути внутрішню. Стрілкові функції роблять цю помилку рідшою: a => b => a + b.

Припущення що JS сам каррінгує функції:

javascript
const add = (a, b) => a + b; const add1 = add(1); // NaN - b стає undefined, а не новою функцією

Нативні функції не каррінгуються самостійно. Потрібно або обгорнути curry-утилітою, або написати вручну.

Відсутній базовий кейс у власному curry:

javascript
// Неправильно - нескінченна рекурсія function badCurry(fn) { return function(...args) { return badCurry(fn)(...args); // немає умови виходу, переповнення стека }; }

Завжди перевіряй args.length >= fn.length перед рекурсивним викликом.

Каррінг варіадичної функції:

javascript
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() може бути швидшим за ручний каррінг, бо це нативна операція.

Приклади

Базовий: адер для повторного використання

javascript
const add = a => b => a + b; const add10 = add(10); // фіксуємо 10, отримуємо адер add10(5); // 15 add10(20); // 30 add10(-3); // 7

add10 - це звичайна функція яка додає 10. Створюєш один раз, використовуєш де потрібно, не чіпаючи оригінальну add.

Середній рівень: Express middleware для авторизації за роллю

javascript
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 для логера

javascript
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) і робить можливим передачу аргументів як по одному, так і групами.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?