Антипаттерны
Функции, которые нельзя каррировать
Универсальный curry опирается на fn.length. Если fn.length не отражает реальное количество аргументов — каррирование сломается.
Rest-параметры
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0)
}
sum.length // 0 — curry не знает, сколько аргументов ждать
// curry(sum)() вызовет sum() сразу — потому что 0 >= 0
Параметры по умолчанию
function createUser(name, role = 'user', active = true) {
return { name, role, active }
}
createUser.length // 1 — только name
// curry(createUser)('Алекс') вызовет функцию сразу,
// не дав передать role и active
arguments
function legacy() {
// Работает с arguments вместо формальных параметров
return Array.from(arguments).join(', ')
}
legacy.length // 0 — curry бесполезен
Каррировать можно только функции с фиксированным количеством параметров, без значений по умолчанию и rest.
Потеря this
Каррирование оборачивает функцию в новые функции. Контекст this теряется:
const obj = {
multiplier: 3,
multiply(a, b) {
return this.multiplier * a * b
},
}
const curriedMultiply = curry(obj.multiply)
curriedMultiply(2)(4) // NaN — this === undefined
Решения:
// 1. Привязать this заранее
const bound = curry(obj.multiply.bind(obj))
bound(2)(4) // 24
// 2. Не использовать this — передавать данные аргументом
const multiply = (multiplier, a, b) => multiplier * a * b
const triple = curry(multiply)(3)
triple(2)(4) // 24
В функциональном программировании предпочитают второй подход: данные — через аргументы, не через this.
Over-currying: слишком длинные цепочки
// Читаемость падает с каждым уровнем
const result = configure(host)(port)(protocol)(path)(headers)(body)(timeout)
// Что делает каждый аргумент? Непонятно без документации
createRequest('api.example.com')(443)('https')('/users')({})('{}')(5000)
Три и более уровней — сигнал к рефакторингу. Используйте объект конфигурации:
// Вместо 7 уровней — объект с именованными параметрами
const createRequest = (config) => {
const { host, port, protocol, path, headers, body, timeout } = config
// ...
}
Исключение: 2-3 уровня, где каждый уровень семантически оправдан (Redux middleware: store => next => action).
Пять причин конфликта каррирования с JavaScript
Доктор Аксель Раушмайер (автор “JavaScript for impatient programmers”) выделяет причины, по которым каррирование в JS неидиоматично:
1. JavaScript — не чистый ФП-язык
В Haskell каррирование встроено в язык — все функции каррированы. В JavaScript это надстройка, требующая явного вызова curry(). Код с каррированием выглядит чужеродно для разработчиков, привыкших к императивному стилю.
2. Функции с переменным числом аргументов
Math.max, console.log, Array.of — множество встроенных функций принимают произвольное число аргументов. Каррирование с ними невозможно.
3. Именованные параметры через объекты
В JavaScript распространён паттерн «options object»:
// Идиоматичный JS
fetch(url, { method: 'POST', headers: { ... }, body: '...' })
// Каррирование не помогает — объект нельзя разбить на цепочку
Каррирование работает с позиционными аргументами. JavaScript-экосистема предпочитает именованные.
4. Сложность типизации
Как показано в предыдущем разделе, точная типизация curry в TypeScript — задача для продвинутых магов типов. В командах это создаёт барьер.
5. Отладка
Стек вызовов каррированной функции — серия анонимных замыканий:
// Стек: один именованный вызов
at volume (app.js:1)
// Стек: каррированная версия — три анонимных
at anonymous (app.js:3)
at anonymous (app.js:2)
at anonymous (app.js:1)
Именуйте промежуточные функции для улучшения читаемости стека:
const curry = fn => function curried(...args) {
// "curried" появится в стеке — уже лучше
return args.length >= fn.length
? fn(...args)
: (...more) => curried(...args.concat(more))
}
Производительность
Каждый уровень каррирования — создание замыкания в куче и проверка args.length >= fn.length. Для единичных вызовов разница нулевая. Для горячих путей:
// Прямой вызов: ~500M операций/сек
const add = (a, b) => a + b
// Каррированный вызов: ~50-100M операций/сек
const curriedAdd = curry((a, b) => a + b)
Разница в 5-10 раз звучит пугающе, но 50M операций в секунду — это 20 наносекунд на вызов. В реальном приложении бутылочное горлышко — DOM, сеть, дисковый I/O, а не 20 наносекунд на вызов функции.
Если ваш код не в тесном цикле, обрабатывающем миллионы элементов, — каррирование не влияет на производительность.
Когда НЕ использовать каррирование
| Ситуация | Почему |
|---|---|
| Команда не знакома с ФП | Каррирование затрудняет чтение кода для тех, кто не знает паттерн |
| Функция с объектом-конфигурацией | Именованные параметры нельзя каррировать осмысленно |
| Rest-параметры, defaults, arguments | fn.length врёт — curry сломается |
| Больше 3 уровней вложенности | Нечитаемо. Используйте объект |
| TypeScript-проект без опыта | Типизация добавит сложность без очевидной пользы |
| Hot path в цикле | 5-10x замедление может быть заметно на миллионах итераций |
Рекомендация
Каррирование — инструмент, не идеология. Используйте его там, где оно делает код проще:
- 2-3 уровня с чётким семантическим смыслом каждого уровня
- Создание специализированных функций из общих
- Построение пайплайнов с
compose/pipe - Middleware-паттерны (Redux, Express)
Не используйте там, где оно усложняет:
- Больше 3 уровней — возьмите объект
- Нетипизируемо в TS — упростите
- Непонятно коллегам — напишите обычную функцию
Итого
| Проблема | Решение |
|---|---|
| Rest-параметры, defaults | Не каррировать такие функции |
Потеря this | bind или передача данных аргументом |
| Длинные цепочки | Объект конфигурации вместо позиционных аргументов |
| Плохие стеки | Именовать промежуточные функции |
| Производительность | Не проблема, кроме hot path на миллионах итераций |
| Команда не знает ФП | Обучить или не использовать |