Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке каррінг у JavaScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Каррінг (currying)** у JavaScript перетворює функцію з кількома аргументами на ланцюг функцій з одним аргументом, де кожен виклик повертає наступну функцію. ```javascript const add = a => b => c => a + b + c; add(1)(2)(3); // 6 const add5 = add(5); // часткове застосування - фіксуємо аргумент add5(3); // 8 add5(10); // 15 ``` **Ключове:** кожен виклик повертає функцію, а не значення, аж до останнього аргументу.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Каррінг (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`) і робить можливим передачу аргументів як по одному, так і групами.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.