Каррирование (карринг) в JavaScript

Нет, термин «каррирование» никак не связан со знаменитой приправой и вообще не имеет никакого отношения к восточной кухне. Это приём в функциональном программировании, позволяющий преобразовать функцию, заменив её несколько первых аргументов константными значениями, тем самым создав новую функцию с меньшим количеством аргументов на основе старой. Этот будет удобно применять в случае, когда первые несколько аргументов функции заранее известны, и указывать их при каждом вызове нет необходимости. Для краткости будем называть преобразовываемую функцию каррируемой, а функцию, которая её преобразовывает, каррирующей.

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

Реализация

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

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

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

После того, как эта функция будет подвержена каррированию с заменой одного аргумента на некоторое значение, можно будет получить функцию, которая, например: инкрементирует (увеличивают на единицу), декрементирует (уменьшают на единицу) или прибавляют к своему уже единственному аргументу заданное заранее значение.

Делается это относительно просто:

function curry(a) {
  return function (b) {
    // в этом вызове аргумент a заменён на переданное в функцию curry значение
    return sum(a, b); 
  };
}
 
var inc = curry(1);
console.log(inc(5)); // 6
 
var dec = curry(-1);
console.log(dec(3)); // 2
 
var plusFive = curry(5);
console.log(plusFive(5)); // 10

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

Рассмотрим ещё одну простую функцию:

function sayTwoWords(word1, word2) {
  console.log(word1 + " " + word2);
}

Немного изменим каррирующую функцию, добавив в качестве параметра каррируемую ей функцию.

function curry(func, a) {
  return function (b) {
    return func(a, b);
  };
}

Теперь функции curry можно передавать каррируемую функцию в качестве аргумента.

var sayHelloTo = curry(sayTwoWords, "Hello");
sayHelloTo("Bob");
sayHelloTo("Marry");
 
var sayGoodbyeTo = curry(sayTwoWords, "Goodbye");
sayGoodbyeTo("Bob");
sayGoodbyeTo("Marry");
 
var minusThree  = curry(sum, -3);
console.log(minusThree(13)); // 10

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

Вспомним, что в js функция при вызове может принимать произвольное количество аргументов, вне зависимости от того, сколько их было указанно при объявлении. Пользуясь этой возможностью, объявим каррирующею функцию с одним параметром func, в котором будет передаваться каррируемая функция. Остальные же параметры будут значениями, на которые будут заменяться первые аргументы каррируемой функции. Эти значения мы будем извлекать из объекта arguments, в котором содержаться все значения аргументов функции, и записывать в массив.

function curry(func) {
  var args = arguments, curryArgs = [];
  /*
   * Записываем значения, на которые 
   * заменим первые аргументы 
   * каррируемой функции в массив.
   */
  for (var i = 1; i < args.length; i++) {
    curryArgs[i - 1] = args[i];
  }
 
  // преобразуем функцию и возвращаем её
}

В возвращаемой функции нам необходимо вызвать каррируемую функцию, подставив в качестве первых аргументов значения из массива curryArgs, а в качестве остальных — значения, переданные в качестве параметров возвращаемой функции. Для этого воспользуемся нативным методом apply, который позволяет вызвать функцию со значениями аргументов, которые переданы ему в массиве. Этот массив метод apply принимает в качестве второго параметра. В качестве первого он принимает объект, который будет возвращаться при обращении к this внутри функции. Перед тем, как вызвать метод apply от каррируемой функции, преобразуем в массив объект arguments возвращаемой функции и склеим, при помощи метода concat, массив curryArgs c полученным массивом. Результат этой операции и передадим в качестве второго аргумента методу apply. В итоге функция curry примет вид:

function curry(func) {
  var args = arguments, curryArgs = [];
 
  for (var i = 1; i < args.length; i++) {
    curryArgs[i - 1] = args[i];
  }
 
  return function () {
    // convert arguments to array
    var argsArr = Array.prototype.slice.call(arguments, 0);    
 
    curryArgs = curryArgs.concat(argsArr);
    return func.apply(this, curryArgs);
  }
}

В качестве завершающего штриха можно проверять является ли первый аргумент функции curry функцией.

function curry(func) {
  var args = arguments, curryArgs = [];
 
  if (typeof func !== 'function') {
    throw new Error('The first arguments must be function!');
  }
 
  for (var i = 1; i < args.length; i++) {
    curryArgs[i - 1] = args[i];
  }
 
  return function () {
    // convert arguments to array
    var argsArr = Array.prototype.slice.call(arguments, 0);    
 
    curryArgs = curryArgs.concat(argsArr);
    return func.apply(this, curryArgs);
  }
}

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

function createElement(tagName, properties) {
  var el = document.createElement(tagName);
  for (var prop in properties) {
    el[prop] = properties[prop];
  }
  return el;
}

Теперь создадим функцию, которая создаёт div c заданными свойствами.

var createDiv = curry(createElement, 'DIV');

Возможно, вам покажется, что проще было бы сделать вот так:

var createDiv = function (properties) {
  return createElement('DIV', properties);
}

Однако этот синтаксис длиннее и нам пришлось указывать второй аргумент каррируемой функции, что кажется мелочью, но всё же излишне. А теперь представьте, что у каррируемой функции список аргументов достаточно длинный или он вовсе переменной длинны. И, хотя каррирование, работает медленнее, оно прельщает своей краткостью и простотой использования.

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

var createMessageBox = curry(createElement, 'DIV', {className: 'message-box'});

Функция createMessageBox будет создавать div c css-классом message-box и не будет принимать никаких параметров.

На этом всё, что я могу вам рассказать про каррирование. Как всегда, желаю вам успехов!