Curry: примеры (JAVASCRIPT)

Техника каррирования функций в JavaScript
Раздел: Функциональное программирование, Каррирование
curry(fn (function), ...args (any)): function

Основные сведения о каррировании

Каррирование (currying) представляет собой функциональную технику, которая преобразует функцию с несколькими аргументами в последовательность функций, каждая из которых принимает один аргумент. Эта концепция названа в честь математика Хаскелла Карри.

Каррирование используется для создания специализированных функций на основе более общих, улучшения переиспользования кода и поддержки point-free стиля программирования. Метод часто применяется в функциональном программировании, обработке событий и конфигурации компонентов.

Функция каррирования в JavaScript не является встроенной в стандартную библиотеку, но реализуется пользователем или используется из библиотек (например, lodash). Классическая реализация принимает функцию и возвращает новую функцию, которая может принимать аргументы по одному или группами. Если переданное количество аргументов меньше ожидаемого, возвращается частично примененная функция, иначе происходит вызов исходной функции.

Аргументы реализации каррирования:

  • func - исходная функция для каррирования
  • arity (опционально) - количество аргументов, которые должна принять функция перед выполнением (по умолчанию используется длина функции func.length)

Возвращаемое значение - каррированная версия исходной функции, которая может принимать аргументы постепенно и возвращает либо результат выполнения (если переданы все аргументы), либо новую каррированную функцию (если аргументов недостаточно).

Простые примеры использования

Базовое каррирование с двумя аргументами:

function add(a, b) {
  return a + b;
}

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

const curriedAdd = curry(add);
console.log(curriedAdd(2)(3));
5

Каррирование с тремя аргументами:

const multiply = curry((a, b, c) => a * b * c);
const multiplyByTwo = multiply(2);
const multiplyByTwoAndThree = multiplyByTwo(3);
console.log(multiplyByTwoAndThree(4));
24

Использование с различным количеством аргументов:

const sum = curry((a, b, c, d) => a + b + c + d);
console.log(sum(1, 2)(3, 4));
console.log(sum(1)(2, 3, 4));
console.log(sum(1)(2)(3)(4));
10
10
10

Похожие функции в JavaScript

Частичное применение (partial application) - функция bind() создает новую функцию с предустановленными аргументами. В отличие от каррирования, где аргументы передаются строго по одному, частичное применение позволяет фиксировать несколько аргументов одновременно.

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

Замыкания (closures) - механизм, который часто используется при реализации каррирования для сохранения состояния аргументов между вызовами.

Функции высшего порядка - принимают или возвращают другие функции, что является основой для реализации каррирования.

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

Аналоги в других языках программирования

Python - functools.partial для частичного применения, каррирование реализуется через лямбда-функции:

from functools import partial
def add(a, b, c):
    return a + b + c

curried_add = lambda a: lambda b: lambda c: add(a, b, c)
result = curried_add(1)(2)(3)
print(result)
6

PHP - замыкания позволяют эмулировать каррирование:

$add = function($a) {
    return function($b) use ($a) {
        return function($c) use ($a, $b) {
            return $a + $b + $c;
        };
    };
};

$result = $add(1)(2)(3);
echo $result;
6

Haskell - каррирование встроено в язык, все функции по умолчанию каррированы:

add a b c = a + b + c
addOne = add 1
addOneTwo = addOne 2
result = addOneTwo 3
-- Результат: 6
6

C# - можно использовать лямбда-выражения:

Func>> curriedAdd = a => b => c => a + b + c;
int result = curriedAdd(1)(2)(3);
Console.WriteLine(result);
6

Распространенные ошибки

Использование функций с переменным числом аргументов без указания arity:

function sum(...args) {
  return args.reduce((acc, val) => acc + val, 0);
}

const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3));
Функция выполнится после первого вызова, так как fn.length = 0

Потеря контекста this при каррировании методов объекта:

const calculator = {
  factor: 10,
  multiply(a, b) {
    return a * b * this.factor;
  }
};

const curriedMultiply = curry(calculator.multiply);
console.log(curriedMultiply(2)(3));
Ошибка: this.factor равно undefined

Каррирование функции, использующей arguments:

function legacy(a, b) {
  console.log(arguments.length);
  return a + b;
}

const curriedLegacy = curry(legacy);
curriedLegacy(1);
arguments работает некорректно при частичном применении

Изменения в последних версиях

В стандарте JavaScript ES6 появились стрелочные функции и rest-параметры, которые упрощают реализацию каррирования. Параметр функции length ведет себя по-разному для функций с значениями по умолчанию и rest-параметрами:

// ES6: стрелочные функции
const curry = (fn) => {
  return function curried(...args) {
    return args.length >= fn.length 
      ? fn(...args) 
      : (...args2) => curried(...args, ...args2);
  };
};

// Разное поведение length
const f1 = (a, b = 1) => a + b;
const f2 = (a, ...rest) => a + rest.length;
console.log(f1.length, f2.length);
1 1

В ES2020 не было внесено специфических изменений для каррирования, но предложение о pipeline operator может улучшить читаемость каррированных функций в будущем.

Расширенные примеры применения

Каррирование в обработчиках событий:

Пример javascript
const handleEvent = curry((eventType, element, handler) => {
  element.addEventListener(eventType, handler);
});

const handleClick = handleEvent('click');
const handleClickOnButton = handleClick(document.getElementById('btn'));
handleClickOnButton((e) => console.log('Клик!'));
Обработчик click события на кнопке

Создание конфигурируемых функций валидации:

Пример javascript
const validate = curry((min, max, value) => {
  return value >= min && value <= max;
});

const validateAge = validate(18, 120);
const validateTemperature = validate(-50, 50);

console.log(validateAge(25));
console.log(validateTemperature(100));
true
false

Композиция каррированных функций:

Пример javascript
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);

const add = curry((a, b) => a + b);
const multiply = curry((a, b) => a * b);
const subtract = curry((a, b) => a - b);

const transform = compose(
  add(10),
  multiply(2),
  subtract(5)
);

console.log(transform(20));
55 // ((20 - 5) * 2) + 10

Ленивые вычисления с каррированием:

Пример javascript
const lazyCurry = (fn) => {
  const curried = (...args) => {
    if (args.length >= fn.length) {
      return fn(...args);
    }
    return Object.assign((...args2) => curried(...args, ...args2), {
      valueOf: () => fn(...args)
    });
  };
  return curried;
};

const calc = lazyCurry((a, b, c) => a * b + c);
const partial = calc(2, 3);
console.log(Number(partial)); // 6 + undefined = NaN
console.log(Number(partial(4))); // 2*3+4 = 10
NaN
10

Каррирование с placeholder для пропуска аргументов:

Пример javascript
const _ = Symbol('placeholder');

const curryWithPlaceholder = (fn) => {
  return function curried(...args) {
    const complete = args.length >= fn.length && !args.includes(_);
    if (complete) return fn.apply(this, args);
    
    return function(...args2) {
      const newArgs = args.map(arg => arg === _ && args2.length ? args2.shift() : arg);
      return curried(...newArgs, ...args2);
    };
  };
};

const pow = curryWithPlaceholder((a, b) => Math.pow(a, b));
const square = pow(_, 2);
const cube = pow(_, 3);

console.log(square(5)); // 25
console.log(cube(3)); // 27
25
27

JS curry function comments

En
Curry Transforms a function to allow partial application