Частичное применение в JavaScript

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

Ну что, давайте приступим и взглянем на полностью выдуманных пример:

function add( a, b ) {
   return a + b;
 }
  
 add( 1, 2 );    // 3
 add( 1, 3 );    // 4
 add( 1, 10 );   // 11
 add( 1, 9000 ); // 9001

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

function add( a, b ) {
   return a + b;
 }
  
 var one = 1;
  
 add( one, 2 );    // 3
 add( one, 3 );    // 4
 add( one, 10 );   // 11
 add( one, 9000 ); // 9001

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

Стандартный сценарий использования

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

// More general function.
  
 function add( a, b ) {
   return a + b;
 }
  
 // More specific functions.
  
 function add1( b ) {
  return add( 1, b );
}
 
function add10( b ) {
  return add( 10, b );
}
 
add( 1, 2 );  // 3
add( 10, 3 ); // 13
 
add1( 2 );    // 3
add1( 3 );    // 4
 
add10( 2 );   // 12
add10( 3 );   // 13

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

Не очень гибкое решение

Поскольку JavaScript имеет функциональную природу, можно создать родовую функцию makeAdder, которая ведет себя следующим образом: Функция makeAdder, когда вызывается с аргументом, возвращает новую функцию. Эта возвращаемая функция, когда вызывается с аргументом, добавляет новое значение к изначально заданному значению, возвращая этот результат. Аргумент, изначально переданный в функцию makeAdder, фактически "заблокирован" в ее значении.

// More-general function.
  
  function add( a, b ) {
   return a + b;
 }
  
 // More-specific function generator.
  
 function makeAdder( a ) {
  return function( b ) {
    return a + b;
  };
}
 
add( 1, 2 );  // 3
add( 10, 3 ); // 13
 
var add1 = makeAdder( 1 );
add1( 2 );    // 3
add1( 3 );    // 4
 
var add10 = makeAdder( 10 );
add10( 2 );   // 12
add10( 3 );   // 13

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

Немного более гибкое решение

Следующий логичны шаг – создание более универсального варианта функции makeAdder,.который не только принимает аргумент, чтобы "блокировать" его, но также принимает функцию, чтобы вызвать ее. В этом случае вы легко сможете повторно использовать эту функциональность, чтобы создать блокированные версии других функций.

// More-general functions.
  
 function add( a, b ) {
   return a + b;
 }
  
 function multiply( a, b ) {
   return a * b;
 }
 
// Relatively flexible more-specific function generator.
 
function lockInFirstArg( fn, a ) {
  return function( b ) {
    return fn( a, b );
  };
}
 
var add1 = lockInFirstArg( add, 1 );
add1( 2 );    // 3
add1( 3 );    // 4
add1( 10 );   // 11
add1( 9000 ); // 9001
 
var mult10 = lockInFirstArg( multiply, 10 );
mult10( 2 );    // 20
mult10( 3 );    // 30
mult10( 10 );   // 100
mult10( 9000 ); // 90000

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

Частичное применение

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

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

Если вы хорошо знакомы с функцией ECMAScript 5 bind, которая позволяет функции иметь не только переопределяемый контекст (значениеthis), но и некоторые заданные аргументы, вы уже имели дело с частичным применением. В данном примере это может помочь думать о this, как о скрытом нулевом аргументе, который применен частично.

Следующий пример гораздо более гибок, чем предыдущие, поскольку в нем используется объект arguments, устанавливающий, как много аргументов должны быть применены частично.

Обратите внимание, что объект arguments является массивоподобным объектом, создаваемым во время вызова функций, доступным только в пределах этой функции, содержащим все аргументы, передаваемые в эту функцию . Хотяargumentsявляется массивоподобным, это не массив. Это значит, что хотя он и имеет свойство.lengthи числовые индексы, у него нет стандартных методов Array.concatили.slice. Чтобы конвертировать объектargumentsв массив, вызывается унаследованныйArray.sliceметод (Array.prototype.slice), как если бы он существовал в объектеarguments, с помощью функции call.

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

function partial( fn /*, args...*/) {
   var aps = Array.prototype.slice,
     args = aps.call( arguments, 1 );
   
   return function() {
     return fn.apply( this, args.concat( aps.call( arguments ) ) );
   };
 }
  
// VERY BORING EXAMPLE
 
function add( a, b ) {
  return a + b;
}
 
var add1 = partial( add, 1 );
add1( 2 ); // 3
add1( 3 ); // 4
add1();    // NaN
 
var add10 = partial( add, 10 );
add10( 2 ); // 12
add10( 3 ); // 13
add10();    // NaN

Это работает, потому что первоначально переданные arguments (без первого аргумента fn, который отделяется) хранятся в виде массива args внутри функции closure, которая создается при вызове функции partial. Поскольку возвращаемая функция имеет доступ к массиву args, каждый раз, когда она вызывается, она вызывает первоначально переданную функцию fn, используя функцию apply. Поскольку apply принимает массив аргументов, а concat объединяет два массива, можно легко вызвать функцию fn с помощью только что переданных arguments, приложенных к изначально заданным аргументам args.

Обратите внимание, что add1() и add10() вызваны без возврата числового аргумента NaN потому что, хотя a является числом, b не определено — undefined, а добавление числа к undefined вычисляет NaN.

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

Function add( a, b ) {
   return a + b;
 }
  
 var return9 = partial( add, 4, 5 );
 return9();       // 9
 return9( 1 );    // 9 - this is like calling add( 4, 5, 1 )
 return9( 9001 ); // 9 - this is like calling add( 4, 5, 9001 )

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

Частичное применение: Варианты

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

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

function partialRight( fn /*, args...*/) {
   var aps = Array.prototype.slice,
     args = aps.call( arguments, 1 );
   
   return function() {
     return fn.apply( this, aps.call( arguments ).concat( args ) );
   };
 }
  
// SOMEWHAT GRATUITOUS EXAMPLE
 
function wedgie( a, b ) {
  return a + ' gives ' + b + ' a wedgie.'
}
 
var joeGivesWedgie = partial( wedgie, 'Joe' );
joeGivesWedgie( 'Ron' ); // "Joe gives Ron a wedgie."
joeGivesWedgie( 'Bob' ); // "Joe gives Bob a wedgie."
 
var joeReceivesWedgie = partialRight( wedgie, 'Joe' );
joeReceivesWedgie( 'Ron' ); // "Ron gives Joe a wedgie."
joeReceivesWedgie( 'Bob' ); // "Bob gives Joe a wedgie."

Хотя функции partial иpartialRight частично применяют аргументы либо к левому, либо к правому краю, ничто не мешает нам сделать еще один шаг и создать функцию, которая позволит вам отобрать необходимые аргументы, чтобы частично их применить. Библиотеки wu.js и Functional Javascript содержат метод под названием partial, который позволит вам сделать это, используя значение заполнителя. В следующем примере я собираюсь назвать эту функцию partialAny.

В этом примере функция partialAny возвращает функцию ƒ, которая, будучи вызванной, вызывает изначально заданную функцию fn с изначально заданными аргументами. Однако любые изначально заданные аргументы "заполнителя" (заданные с помощью partialAny._) будут заменены по порядку аргументами, переданными при вызове ƒ. Любые оставшиеся аргументы, переданные в ƒ, добавляются в конец.

Примечание: изучите выражения немедленно вызванной функции, если вы незнакомы с синтаксисом(function(){ /* code */ })();.

var partialAny = (function(aps){
   
   // This function will be returned as a result of the immediately-
   // invoked function expression and assigned to the `partialAny` var.
   function func( fn /*, args...*/) {
     var argsOrig = aps.call( arguments, 1 );
     
     return function() {
       var args = [],
        argsPartial = aps.call( arguments ),
        i = 0;
      
      // Iterate over all the originally-specified arguments. If that
      // argument was the `partialAny._` placeholder, use the next just-
      // passed-in argument, otherwise use the originally-specified
      // argument.
      for ( ; i < argsOrig.length; i++ ) {
        args[i] = argsOrig[i] === func._
          ? argsPartial.shift()
          : argsOrig[i];
      }
      
      // If any just-passed-in arguments remain, add them to the end.
      return fn.apply( this, args.concat( argsPartial ) );
    };
  }
  
  // This is used as the placeholder argument.
  func._ = {};
  
  return func;
})(Array.prototype.slice);
 
// SLIGHTLY MORE LEGITIMATE EXAMPLE
 
function hex( r, g, b ) {
  return '#' + r + g + b;
}
 
var redMax = partialAny( hex, 'ff', partialAny._, partialAny._ );
redMax( '11', '22' ); // "#ff1122"
 
// Because `__` is easier on the eyes than `partialAny._`, let's use
// that instead. This is, of course, entirely optional, and the name
// could just as well be `foo` or `PLACEHOLDER` instead of `__`.
 
var __ = partialAny._;
 
var greenMax = partialAny( hex, __, 'ff' );
greenMax( '33', '44' ); // "#33ff44"
 
var blueMax = partialAny( hex, __, __, 'ff' );
blueMax( '55', '66' ); // "#5566ff"
 
var magentaMax = partialAny( hex, 'ff', __, 'ff' );
magentaMax( '77' ); // "#ff77ff"

Хотя некоторые библиотеки используют функциональность partialAny как partial, они могут использовать только это название, так как в них нет другой функции под названием partial. Вот почему они называют эту функциональность curry, но в большинстве случаев это неверное имя, учитывая, что их функция curry делает.

Поскольку существует определенная путаница вокруг понятия каррирования, я попытаюсь внести некоторую ясность.

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

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

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

Следующая функция curry возвращает функцию ƒ, которая, будучи вызванной, сначала проверяет, все ли аргументы функции fn были удовлетворены. Если это так, вызывается fn с изначально заданными аргументами, за которыми следуют все аргументы, переданные в ƒ. Если результат отрицательный, возвращается функция ƒ1 (рекурсивно), которая ведет себя подобно функции f. Только когда все аргументы функцииfn удовлетворены, вызываетсяfn.

Семантически, следующая реализация curry лучше всего описывается, как "преобразование функции N аргументов таким образом, чтобы ее можно было назвать последовательностью функций, каждая из которых принимает ноль и более аргументов, пока все N аргументов не будут удовлетворены". Хотя это формально отличается от определения каррирования, такая реализация может соперничать с каррированием, и даже оказаться более гибким (это возможно, так как JavaScript сам по себе очень гибок).

Обратите внимание, что хотя функции имеют свойство.length, задающее количество аргументов, ожидаемых функцией, при некоторых условиях JavaScript не может определить количество ожидаемых аргументов (например, когда функция использует объектargumentsвместо отдельных аргументов). В этом случае вы можете задать числовое значениеnдо функции, которая будет использоваться, заменив им свойствоfn.length.

function curry(/* n,*/ fn /*, args...*/) {
   var n,
     aps = Array.prototype.slice,
     orig_args = aps.call( arguments, 1 );
   
   if ( typeof fn === 'number' ) {
     n = fn;
     fn = orig_args.shift();
   } else {
    n = fn.length;
  }
  
  return function() {
    var args = orig_args.concat( aps.call( arguments ) );
    
    return args.length < n
      ? curry.apply( this, [ n, fn ].concat( args ) )
      : fn.apply( this, args );
  };
}
 
// TOTALLY CONTRIVED EXAMPLE
 
var i = 0;
function a( x, y, z ) {
  console.log( ++i + ': ' + x + ' and ' + y + ' or ' + z );
};
 
a( 'x', 'y', 'z' );     // "1: x and y or z"
 
var b = curry( a );
b();                    // nothing logged, `a` not invoked
b( 'x' );               // nothing logged, `a` not invoked
b( 'x', 'y' );          // nothing logged, `a` not invoked
b( 'x' )( 'y' );        // nothing logged, `a` not invoked
b( 'x' )( 'y' )( 'z' ); // "2: x and y or z"
b( 'x', 'y', 'z' );     // "3: x and y or z"
 
var c = curry( a, 'x' );
c();                    // nothing logged, `a` not invoked
c( 'y' );               // nothing logged, `a` not invoked
c( 'y', 'z' );          // "4: x and y or z"
c( 'y' )( 'z' );        // "5: x and y or z"
 
var d = curry( c, 'y' );
d();                    // nothing logged, `c` not invoked
d( 'z' );               // "6: x and y or z"
 
var e = curry( a, 'x', 'y' );
e();                    // nothing logged, `a` not invoked
e( 'z' );               // "7: x and y or z"
 
var f = curry( a, 'x', 'y', 'z' );
f();                    // "8: x and y or z"
 
// THE OPTIONAL `n` ARGUMENT
 
function aNoLength() {
  var x = arguments[0], y = arguments[1], z = arguments[2];
  console.log( ++i + ': ' + x + ' and ' + y + ' or ' + z );
};
 
// You must specify `n` of 3 here since aNoLength.length === 0!
 
var g = curry( 3, aNoLength );
g();                    // nothing logged, `aNoLength` not invoked
g( 'x' );               // nothing logged, `aNoLength` not invoked
g( 'x', 'y' );          // nothing logged, `aNoLength` not invoked
g( 'x' )( 'y' );        // nothing logged, `aNoLength` not invoked
g( 'x' )( 'y' )( 'z' ); // "9: x and y or z"
g( 'x', 'y', 'z' );     // "10: x and y or z"
 
var h = curry( 3, aNoLength, 'x' );
h();                    // nothing logged, `a` not invoked
h( 'y' );               // nothing logged, `a` not invoked
h( 'y', 'z' );          // "11: x and y or z"
h( 'y' )( 'z' );        // "12: x and y or z"

Частичное применение против каррирования

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

function add( a, b, c ) {
   var total = a + b + c;
   return a + '+' + b + '+' + c + '=' + total;
 }
  
 add( 1, 2, 3 ); // "1+2+3=6"
  
 // Partial application
  
var add1partial = partial( add, 1 );
add1partial( 2, 3 ); // "1+2+3=6"
add1partial( 2 );    // "1+2+undefined=NaN"
 
// Currying
 
var add1curry = curry( add, 1 );
add1curry( 2, 3 ); // "1+2+3=6"
add1curry( 2 );    // a function is returned (what?!)

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

Но возвращаемая функция, как и любая другая функция, может быть назначена переменной и вызвана, вот так:

function add( a, b, c ) {
   var total = a + b + c;
   return a + '+' + b + '+' + c + '=' + total;
 }
  
 var add1curry = curry( add, 1 );
 add1curry( 2, 3 );  // "1+2+3=6"
 add1curry( 4, 5 );  // "1+4+5=10"
  
var add1and2curry = add1curry( 2 );
add1and2curry( 3 ); // "1+2+3=6"
add1and2curry( 4 ); // "1+2+4=7"

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

curry( add, 1 )( 2 )( 3 );             // "1+2+3=6"
 curry( add, 1, 2 )( 3 );               // "1+2+3=6"
 curry( add, 1, 2, 3 )();               // "1+2+3=6"
 curry( add )( 1 )( 2 )( 3 );           // "1+2+3=6"
 curry( add, 1 )()( 2 )()( 3 );         // "1+2+3=6"
 curry( add )()( 1 )()( 2 )()( 3 );     // "1+2+3=6"
 curry( add, 1 )()()()( 2 )()()()( 3 ); // "1+2+3=6"

Возможно последний пример не совсем практичный, но согласитесь, выглядит здорово.

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

Хотя каррирование в JavaScript возможно, оно не столь применимо, как в некоторых других языках функционального программирования, например, в ML и Haskell. В основном, это вызвано тем фактом, что функции JavaScript ведут себя по-другому, нежели в тех, других языках.

В книге Учим Haskell, в главе Функции высшего порядка это объясняется так: "Формально каждая функция в Haskell принимает только один параметр. Так как же это возможно, что мы до сих пор определяли и использовали несколько функций, которые принимают более одного параметра? Что ж, это ловкий трюк! Все функции, которые до сих пор принимали несколько параметров, были подвергнуты каррированию."

Оказывается, что в тех, других языках определение функции, которая принимает множество аргументов, – это синтаксический сахар для определения серий функций, каждая из которых принимает единственный аргумент, так как каррирование встроено в язык на самом низком уровне. Вот почему в тех языках "функция" значит не совсем то же самое, что в JavaScript. Это больше похоже на функцию в математике, где вы подставляете значение и получаете некий результат, как например с ƒ(x) = x + 1.

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

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

В заключении:

Частичное применение чаще всего используется для применения одного или более аргументов в начале функции, как это было в примерах с partial. Его пользу проще всего увидеть в функции ECMAScript 5 bind, которая позволяет функции иметь не только переопределяемый контекст, но и частично применяемые аргументы в начале.

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

Хотя я и описал здесь полезные служебные функции, советую ознакомиться с хорошо документированными и популярными библиотеками wu.js и Functional Javascript, а также с популярной библиотекой Underscore.js, так как они поддерживают большую часть, если не всю функциональность, которая обычно требуется для использования частичного применения в JavaScript.