Learning Book

Что это и зачем

Определение

Каррирование превращает функцию f(a, b, c) в f(a)(b)(c). Не вызывает функцию — трансформирует её. Из одной функции с N аргументами получается N вложенных функций, каждая принимает ровно один аргумент.

// Обычная функция
function add(x, y) {
  return x + y
}
add(2, 3) // 5

// Каррированная версия
function curriedAdd(x) {
  return function (y) {
    return x + y
  }
}
curriedAdd(2)(3) // 5

Стрелочная запись делает каррирование визуально компактным:

const add = x => y => x + y
add(2)(3) // 5

Имя и происхождение

Термин назван в честь Хаскелла Карри — математика, исследовавшего комбинаторную логику. Но саму идею впервые описал Моисей Шёнфинкель в 1924 году. Карри развил её позже, и название закрепилось за ним.

Язык программирования Haskell тоже назван в честь Карри. В Haskell все функции каррированы по умолчанию.

Связь с замыканиями

Каждый вызов в цепочке создаёт замыкание, которое захватывает переданный аргумент:

const add = x => y => x + y

const add5 = add(5)
// add5 — это замыкание: (y) => 5 + y
// Переменная x = 5 захвачена из внешней области видимости

add5(3) // 8
add5(10) // 15

add(5) возвращает новую функцию, замкнутую на x = 5. Эта функция живёт, пока на неё есть ссылка, — точно как описано в главе про замыкания.

Зачем каррировать

Создание специализированных функций

Из одной общей функции порождаем семейство конкретных:

// Общая функция логирования
const log = level => message => `[${level}] ${message}`

// Специализированные логгеры
const warn = log('WARN')
const error = log('ERROR')
const info = log('INFO')

warn('Диск почти заполнен')  // "[WARN] Диск почти заполнен"
error('Соединение потеряно') // "[ERROR] Соединение потеряно"
info('Сервер запущен')       // "[INFO] Сервер запущен"

Не нужно каждый раз передавать 'WARN' — он зафиксирован при создании warn.

Переиспользование конфигурации

// Каррированный запрос к API
const request = baseUrl => endpoint => params =>
  fetch(`${baseUrl}${endpoint}?${new URLSearchParams(params)}`)

// Фиксируем базовый URL
const api = request('https://api.example.com')

// Фиксируем эндпоинт
const getUsers = api('/users')
const getPosts = api('/posts')

// Вызываем с конкретными параметрами
getUsers({ page: 1, limit: 10 })
getPosts({ category: 'js' })

Подготовка функций для compose/pipe

Каррированные функции принимают по одному аргументу — их легко выстраивать в цепочки. Подробнее — в разделе compose и pipe.

Каррирование — не вызов

Частая ошибка: путать каррирование с немедленным вызовом. Каррирование ничего не вычисляет — оно создаёт новую структуру из функций.

const multiply = x => y => x * y

// Каррирование: создаём специализированную функцию
const double = multiply(2) // (y) => 2 * y — пока ничего не вычислено

// Вызов: вычисляем результат
double(10) // 20

Каррирование пришло из лямбда-исчисления Алонзо Чёрча (1930-е). В лямбда-исчислении все функции принимают ровно один аргумент — это фундаментальное ограничение. Функция от двух аргументов λxy.x+y — это синтаксический сахар для λx.(λy.x+y), то есть каррированная форма. JavaScript не имеет такого ограничения, поэтому каррирование — осознанный выбор, а не необходимость.

Итого

ФактОписание
Что этоТрансформация f(a, b, c) в f(a)(b)(c)
Что делаетНе вызывает функцию, а создаёт цепочку из вложенных функций
МеханизмКаждый вызов создаёт замыкание, захватывающее аргумент
ЗачемСпециализация, переиспользование конфигурации, подготовка к compose/pipe
ПроисхождениеЛямбда-исчисление Чёрча, назван в честь Хаскелла Карри