Learning Book

Антипаттерны

Функции, которые нельзя каррировать

Универсальный 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, argumentsfn.length врёт — curry сломается
Больше 3 уровней вложенностиНечитаемо. Используйте объект
TypeScript-проект без опытаТипизация добавит сложность без очевидной пользы
Hot path в цикле5-10x замедление может быть заметно на миллионах итераций

Рекомендация

Каррирование — инструмент, не идеология. Используйте его там, где оно делает код проще:

  • 2-3 уровня с чётким семантическим смыслом каждого уровня
  • Создание специализированных функций из общих
  • Построение пайплайнов с compose/pipe
  • Middleware-паттерны (Redux, Express)

Не используйте там, где оно усложняет:

  • Больше 3 уровней — возьмите объект
  • Нетипизируемо в TS — упростите
  • Непонятно коллегам — напишите обычную функцию

Итого

ПроблемаРешение
Rest-параметры, defaultsНе каррировать такие функции
Потеря thisbind или передача данных аргументом
Длинные цепочкиОбъект конфигурации вместо позиционных аргументов
Плохие стекиИменовать промежуточные функции
ПроизводительностьНе проблема, кроме hot path на миллионах итераций
Команда не знает ФПОбучить или не использовать