Шпаргалка по каррированию: что это, зачем нужно, примеры и практические случаи.

Каррирование в JavaScript

Каррирование — это преобразование функции от нескольких аргументов в цепочку функций, каждая из которых принимает один аргумент.

Названо в честь математика Хаскелла Карри. В функциональном программировании это базовый приём: он позволяет собирать новые функции из старых, не дублируя код.

Обычная функция vs каррированная

// обычная функция
function add(a, b, c) {
    return a + b + c;
}

add(1, 2, 3); // 6

// каррированная версия
function addCurried(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        };
    };
}

addCurried(1)(2)(3); // 6

Каждый вызов возвращает новую функцию, которая «помнит» предыдущий аргумент через замыкание.

Зачем это нужно

  • Частичное применение — можно зафиксировать часть аргументов и получить новую функцию.
  • Переиспользование — одна каррированная функция превращается в набор специализированных.
  • Композиция — каррированные функции удобно соединять в цепочки.
  • Читаемость — код становится декларативнее, когда логика собирается из маленьких кирпичиков.

Пример: частичное применение

const multiply = (a) => (b) => a * b;

const double = multiply(2);
const triple = multiply(3);

double(5); // 10
triple(5); // 15

Мы один раз описали умножение, а потом получили сколько угодно «специализированных» функций — без дублирования кода.

Пример: каррирование для работы с массивами

const map = (fn) => (arr) => arr.map(fn);
const filter = (fn) => (arr) => arr.filter(fn);
const reduce = (fn, init) => (arr) => arr.reduce(fn, init);

const doubleAll = map((x) => x * 2);
const keepEven  = filter((x) => x % 2 === 0);
const sum       = reduce((a, b) => a + b, 0);

const nums = [1, 2, 3, 4, 5];

doubleAll(nums);        // [2, 4, 6, 8, 10]
keepEven(nums);         // [2, 4]
sum(nums);              // 15

// композиция
sum(keepEven(doubleAll(nums))); // 2 + 4 + 6 + 8 + 10 = 30

Такой стиль позволяет собирать обработку данных из готовых «полуфабрикатов», как из конструктора.

Универсальная функция curry

Писать каррирование вручную утомительно. Обычно используют вспомогательную функцию, которая делает это автоматически.

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function (...rest) {
            return curried.apply(this, args.concat(rest));
        };
    };
}

const add3 = curry((a, b, c) => a + b + c);

add3(1)(2)(3);     // 6
add3(1, 2)(3);     // 6
add3(1)(2, 3);     // 6
add3(1, 2, 3);     // 6

Функция работает с любым количеством аргументов и позволяет вызывать каррированную версию как угодно: по одному, группами или сразу всеми.

Каррирование и композиция

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

const compose = (...fns) => (x) =>
    fns.reduceRight((acc, fn) => fn(acc), x);

const trim      = (s) => s.trim();
const lower     = (s) => s.toLowerCase();
const replace   = (from) => (to) => (s) => s.split(from).join(to);

const slugify = compose(
    replace(' ' )('-'),
    lower,
    trim
);

slugify('  Hello World  '); // "hello-world"

Обрати внимание на replace: она каррирована, поэтому её легко вставить в цепочку, зафиксировав одни аргументы и оставив «свободным» входное значение.

Пример: работа с API и конфигом

const request = (baseUrl) => (headers) => (endpoint) => (options) =>
    fetch(baseUrl + endpoint, {
        ...options,
        headers: { ...headers, ...options?.headers },
    });

// фиксируем базовый URL и заголовки один раз
const api = request('https://api.example.com')({
    'Content-Type': 'application/json',
});

// теперь работаем только с endpoint'ами
const getUser   = api('/user');
const getPosts  = api('/posts');
const getPost   = (id) => api(`/posts/${id}`)({ method: 'GET' });

getUser({ method: 'GET' });
getPost(42)({ method: 'GET' });

Это классический паттерн для HTTP-клиентов: конфигурация фиксируется один раз, а дальше код работает с короткими специализированными функциями.

Каррирование vs частичное применение

Эти термины часто путают, но разница есть.

  • Каррирование — всегда превращает функцию от N аргументов в N функций по одному аргументу.
  • Частичное применение — фиксирует любое количество аргументов и возвращает функцию, ждущую остальные.
// каррирование: строго по одному
const curried = (a) => (b) => (c) => a + b + c;
curried(1)(2)(3);

// частичное применение: можно фиксировать сколько угодно
function partial(fn, ...fixed) {
    return (...rest) => fn(...fixed, ...rest);
}

const add = (a, b, c) => a + b + c;
const addOne = partial(add, 1);
addOne(2, 3); // 6

На практике их часто смешивают и называют «каррированием» оба приёма. Это нормально, но полезно понимать разницу.

Когда каррирование не нужно

  • Простые функции без повторного использования — если функция вызывается один раз, каррировать её незачем.
  • Горячий код — каждая обёртка создаёт замыкание, а это небольшие накладные расходы. В критичных местах иногда проще написать обычную функцию.
  • Команда не знает FP — каррирование требует привычки. Если коллеги не в теме, код может показаться «магическим».

Что запомнить

  • Каррирование — это f(a, b, c) → f(a)(b)(c).
  • Главная польза — частичное применение и композиция.
  • Пиши универсальную curry() один раз и используй везде.
  • Особенно хорошо работает в пайплайнах обработки данных и в конфигурации API-клиентов.
  • Не каррируй ради каррирования — только там, где это упрощает код.