Каррирование (карринг) в 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 и не будет принимать никаких параметров.

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

  • Поручик Ржевский

    Крайне омерзительная статья!!! Писатель сей отсебятины видимо либо не на Руси родился, либо русский язык недавно начал узучать!

    Мне тошно было читать эту американщину!!! Что ещё за «карирующая«?! С головой вообще дружите?! В русском языке есть столько прекрасных и понятных слов, а вы какую-то американскую блевотину сюда лепите!

    Неужели нельзя было взять слово «Преобразующая»?

    «Употреблять иностранное слово, когда есть равносильное ему руское слово, — значит оскорблять и здравый смысл, и здравый вкус» — © В.Г. Белинский

    Берегите чистоту языка, как святыню! Никогда не употребляйте иностранных слов. Русский язык так богат и гибок, что нам нечего брать у тех, кто беднее нас. — © И. Тургенев

    Берегите наш язык, наш прекрасный русский язык,— это клад, это достояние, переданное нам нашими предшественниками! Обращайтесь почтительно с этим могущественным орудием. © И. Тургенев

    Во дни сомнений, во дни тягостных раздумий о судьбах моей родины, — ты один мне поддержка и опора, о великий, могучий, правдивый и свободный русский язык! Не будь тебя — как не впасть в отчаяние при виде всего, что совершается дома? Но нельзя верить, чтобы такой язык не был дан великому народу! — © И. Тургенев

    «Нравственность человека видна в его отношении к слову» — © Л.Н. Толстой (1828-1910)

    • bravchik

      Вы идиот. Слово «каррирование» пошло от имени Карри Хаскель. При чем здесь «преобразование» ?

      • Даниил Данилов

        Зачем оскорблять и за мелочи. Нельзя просто поправить, что ли

        • bravchik

          Прошу прощения, не хотел никого обидеть.

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

  • Sonlix

    Благодарю за статью, наконец-то разобрался с каррированием.

    > Поручик Ржевский
    А по делу есть что? Ты бы еще ворвался на форум полиглотов, и начал их обвинять в произношении иностранных слов.

  • Sonlix

    Дополню статью, прототипным наследованием:

    Function.prototype.curry = function () {
    var slice = Array.prototype.slice,
    args = slice.call(arguments, 0),
    that = this; // соглашение имен

    return function(){
    return that.apply(null, args.concat(slice.call(arguments, 0)));
    }
    }

    // функция, которую будем каррировать
    function sum (x, y) {
    return x + y;
    }

    var cur = sum.curry(3);
    var res = cur(5); // 8

  • А чем вас не устроил `Function.prototype.bind()`? https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind

    Было бы здорово дать примеры реализаций каррирования в библиотеках (_.partial в lodash/underscore, _.partialRight в lodash)

    И вы называете каррированием (функция берет свои аргументы по одному) частичное применение функции (фиксация части аргументов функции).

    https://ru.wikipedia.org/wiki/Каррирование
    https://ru.wikipedia.org/wiki/%D0%A7%D0%B0%D1%81%D1%82%D0%B8%D1%87%D0%BD%D0%BE%D0%B5_%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5