El objeto arguments en Javascript
Currying y aplicaciones parciales

21 Ene 2011

En Javascript, el objeto arguments es un tipo especial de datos que se genera en el interior de cada función recogiendo todos los parámetros (argumentos) que se han enviado a la misma.

Como ocurre con los Arrays, este objeto nos permite conocer el número de elementos que contiene mediante la propiedad length. También podemos acceder al valor de los argumentos utilizando su correspondiente índice numérico:

function testLength(){
  return arguments.length;
}
function testAccess(){
  return 'The first arg is: ' + arguments[0] + ', and the last is: ' + arguments[arguments.length - 1];
}
console.log( testLength( 'foo', 'bar', null, [ 1, 2, 3 ] ) ); // 4
console.log( testAccess( 'foo', 'bar', null, [ 1, 2, 3 ] ) );
// The first arg is: foo, and the last is: 1,2,3

Pese a su parecido con los Arrays, no debemos olvidar que se trata de un objeto:

function test(){
  return ( Object.prototype.toString.call( arguments ) );
}
console.log( test( 'foo', 'bar', null, [1,2,3]) ); // [object Object]

Particularidades del objeto arguments:

  1. El objeto arguments no es creado automáticamentecada vez que invocamos una función. Sólo se genera si es requerido, por lo que su uso, supone una penalización (mínima) de rendimiento.
  2. El objeto arguments no se crea si ya exite un parámentro formal con este nombre o si hemos declarado una variable homónima en el interior de la función.
    function test( a, arguments ){
     return arguments;
    }
    console.log( test( 1 ) ); // undefined
    console.log( test( 1, 2 ) ); // 2
     
    function test( a, b ){
     var arguments = 'foo';
     return arguments;
    }
    console.log( test( 1, 2 ) );  // foo

OK, ya conocemos la teoría. Veámos casos prácticos.

Ya que Javascript es un lenguaje muy flexible a la hora de invocar funciones, arguments puede resultar muy útil cuando no conocemos con precisión el número de argumentos y queremos operar con ellos. Un caso típico, puede ser el necesitar de una función sumatoria:

function fnSum(){
  for( var i = 0, result = 0, j = arguments.length; i < j; i++){
    result += arguments[i];
  }
  return result;
}
console.log( fnSum( 1, 4, 6, 3, 5, 6, 3 ) ); // 28

El ejemplo anterior resulta tan sencillo como ilustrativo: recorremos el objeto arguments como si se tratase de un Array sumando el valor de cada argumento al anterior.

Probemos con algo más útil: una función que calcule la media aritmética con aquellos números que le suministremos.

var fnAverage = function() {
  for ( var i = 0, total = 0, j = arguments.length; i < j; i++ ) {
    total += arguments[ i ];
  }
 return total / arguments.length;
}
console.log( fnAverage( 6, 8, 6, 9, 9, 10 ) ); // 8

Demasiado fácil, seguro que hay más posibilidades…

Tomemos como ejemplo una función que precisa de un determinado número de parámetros para actuar. Para evitar obtener un valor erróneo en caso de que falte alguno, podemos crear una segunda función que envuelva a la original y que verifique el número de argumentos.

(Gracias a Angus Croll por el esquema para el siguiente ejemplo)

var testArgs = function (fn){
  return function(){
    if( arguments.length < fn.length ){
      throw( [ "Expected", fn.length, " arguments, got ", arguments.length ] );
    }
    return fn.apply( this, arguments );
  }
}
 
var areaTriangle = testArgs( function( tBase, tHeight ){
  return ( tBase * tHeight ) / 2;
} );
 
console.log( areaTriangle( 10 ) ); // ["Expected", 2, " arguments, got ", 1]
console.log( areaTriangle( 10, 5 ) ); // 25

Como veremos a continuación, puede resultar interesante convertir el objeto arguments en un verdadero Array para poder utilizar sus métodos nativos (concat, shift, pop, …). Una de las formas de conseguirlo es mediante la herencia de prototipos (o prototípica):

function test(){
  var aps = Array.prototype.slice,  // Cacheamos el método nativo.
  args = aps.call( arguments );
  return ( Object.prototype.toString.call( args ) );
}
 
console.log( test( 1, 4, 6, 3, 5, 6, 3 ) ); // [object Array]

Conseguido! Ahora que hemos expandido las posibilidades nativas del objeto arguments convirtiéndolo en un Array genuino, podemos plantearnos patrones de diseño más complejos.

CURRYING

En informática, el currying es un paradigma de la Programación Funcional que consiste en convertir una función de n argumentos en n funciones de un solo argumento cada una.
Para implementar correctamente el currying, tenemos que tener en cuenta que al invocar la función ‘arrastrada‘ (curried), debemos suministrar todos los argumentos requeridos para obtener como respuesta otra función. En ese momento, la función orignal es invocada y obtenemos su resultado. Esto significa que, si omitimos algunos de los argumentos, la función original nunca será ejecutada.

Extendiendo la definición, esta técnica se suele utilizar para fijar parámetros de una función en tiempo de ejecución de tal modo que, cada vez que sea invocada, ya cuente con ciertos argumentos preasignados.

Necesitaremos concatenar los parámetros que luego pasaremos a la función original, por lo que retomaremos el ejemplo anterior para convertir el objeto arguments en un verdadero Array.

Function.prototype.curry = function(){
  var n,
  aps = Array.prototype.slice,
  orig_args = aps.call( arguments ),
  __method = this;
 
  if ( arguments.length < 1 ) {
    return this; //nothing to curry with - return function
  }
 
  if ( typeof __method === 'number' ) {
    n = __method;
    __method = orig_args.shift();
  } else {
    n = __method.length;
  } 
 
  return function() {
    var args = orig_args.concat( aps.call( arguments ) );
    return args.length < n
      ? __method
      : __method.apply( this, args );
  };
}
 
function writeSeq( start, end ) {
  for( var i = start; i < end; ++i ) {
    console.log( i );
  }
}

La función writeSeq, escribe la secuencia de números comprendida entre el valor de sus argumentos start y end.

writeSeq( 1, 5 ); // 1, 2, 3, 4
 writeSeq( 10, 16 ); // 10, 11, 12, 13, 14, 15

Para fijar el valor del primer argumento, utilizaríamos el currying:

var seq20 = writeSeq.curry( 20 );
 seq20( 25 ); // 20, 21, 22, 23, 24 
 
var seq35 = writeSeq.curry( 35 );
seq35( 40 ); // 35, 36, 37, 38, 39

Si omitimos el segundo argumento, devolvemos una función pero, al estar incompleta, no se invoca a la origianl y, por tanto, no obtenemos ningún valor.

seq35(); // writeSeq( start, end ) Obtenemos la función pero ningún valor.

Con esta técnica, hemos fijado uno de los argumentos de una función utilizando otra. Sin embargo, se precisan de todos los argumentos para obtener un resultado. Si necesitáramos más flexibilidad, podemos recurrir a otro paradigma de la programación funcional: las aplicaciones parciales.

Aplicaciones parciales en Javascript.

Una aplicación parcial, se define como el proceso de fijar valores a los argumentos de una función antes de que sea invocada. De hecho, la aplicación parcial es en sí misma una función que devuelve otra función nueva con algunos parámetros ya asignados.

La diferencia es que, mientras el currying precisa de todos los argumentos para invocar a la función orginal, la aplicación parcial llamará siempre a la función aún faltando dichos argumentos. En caso omisión, el resultado será el valor undefined.

Function.prototype.bind = function( ){
  var  aps = Array.prototype.slice,
  args = aps.call( arguments ),
  __method = this;
  return function() {
    return __method.apply( this, args.concat( aps.call( arguments ) ) );
  };
}
 
var converter = function( ratio, symbol, input ) {
  return [ (input * ratio ).toFixed( 1 ), symbol ].join( " " );
}
 
var sum = function( a, b ){
  return ( a + b );
 }
 
var kilosToPounds = converter.curry( 2.2, "lbs" );
var milesToKilometers = converter.curry( 1.62, "km" );
 
console.log( kilosToPounds( 4 ) ); // 8.8 lbs
console.log( milesToKilometers( 34 ) ); // 55.1 km
 
var add10 = sum.curry( 10 );
console.log( add10( 5 ) ); // 15
console.log( add10( 15 ) ); // 25
 
var add20 = sum.curry( 20 );
console.log( add20( 10 ) ); // 30
console.log( add20( 15 ) ); // 35

Si omitiéramos el ‘segundo‘ parámetro (necesario), la función original sería invocada de todos modos, obteniedo NaN como resultado. En Javascript, la propiedad NaN es el resultado de evaluar una operación aritmética entre un número (a) y un valor undefined (b):

console.log( kilosToPounds() ); // NaN lbs
console.log( add20() );  // NaN

Las aplicaciones parciales, como hemos comprobado, resultan más flexibes que el currying al no necesitar de todos los argumentos para invocar a la función: siempre obtendremos un valor.

Conclusión

Como hemos podido estudiar, el objeto arguments ofrece mucha flexibilidad a la hora de crear funciones más flexibles o en el diseño de patrones como el currying o las aplicaciones parciales.
Originalmente, este objeto comparte similitudes con un Array convencional pero sin llegar a serlo: podemos conocer el número de elementos que lo componen (mediante lenght) y acceder a sus valores utilizando un índice. Sin embargo, implementar funcionalidades más avanzadas puede requerir que convirtamos este objeto en un verdadero Array. Para ello, podemos servirnos de diversas técnicas como la herencia de prototipos que hemos descrito más arriba.
Convertido este objeto en un Array, ganamos una enorme flexibilidad en el diseño de funciones resultando fácil implementar técnicas como el currying y las aplicaciones parciales en Javascript.

Pese a su funcionalidad, Brendan Eich, el creador de Javascript, ha expresado varias veces su intención de retirar progresivamente el objeto arguments del lenguaje. Su sucesor, según el borrador de la nueva especificación ECMAScript –Harmony-, sera el concepto ‘rest_parameters‘ (parametros restantes).
La idea detrás de ‘rest_parameters’ es muy simple: añadiendo el prefijo ‘…’ al último parámetro (o al único) de una función, lo convertimos en un array (un genuino Array) que actúa como contenedor para el resto de argumentos suministrados que no estén emparejados en la función a la que llaman.

El siguiente ejemplo también ha sido planteado por Angus Croll:

var test( fn, ...args ) {
  return fn.apply( args );
}
callMe( string.concat, 'Super', 'califragilistice', 'xpialidocious' );
// Supercalifragilisticexpialidocious

En el ejemplo anterior, la llamada a la función callMe suministra cuatro parámetros. Sin embargo, la función solo espera uno (fn) y prepara un contenedor (un Array) para el resto (strings en este caso).

Firefox tiene previsto implementar los ‘rest_parameters‘ a lo largo de 2011. Mientras tanto, podemos seguir utilizando el objeto arguments gracias al cual conseguimos patrones de diseño avanzado como el currying o las aplicaciones parciales en Javascript.

Más información

Mike Hadlow, Currying vs Partial Function Application
Angus Croll, The Javascript arguments object… and beyond
«Cowboy» Ben Alman, Partial Application in Javascript
Felix Geisendörfer, Turning Javascript’s arguments object into an Array

Más:

{4} Comentarios.

  1. anje

    Hola
    Un buen tema, de pocos en español, enfocados a javascript.
    Tengo algunas dudas, en la siguiente funcion en la cual conviertes argumentos en arrays:

    function test(){
    var aps = Array.prototype.slice, // Cacheamos el método nativo.
    args = aps.call( arguments );
    return ( Object.prototype.toString.call( args ) );
    }

    Que como trabajan esta dos lineas:

    var aps = Array.prototype.slice, // Cacheamos el método nativo. ??
    args = aps.call( arguments ); ??

    Que me parece que es el trozo de codigo que hace la MAGIA, que por cierto, esto lo he visto en algunos lugares pero nunca lo he llegado a entender a profundidad. Lo he visto asi:
    var args=Array.prototype.slice.call(arguments)
    ó
    var args=[].prototype.slice.call(arguments)
    Y con algunas otras variantes. pero siempre me ha intrigado.
    Saludos.
    GRACIAS.

    • Carlos Benítez

      Hola Anje,
      efectivamente, la ‘magia’ está en las dos líneas que has extraído. De ellas, la primera parece la más complicada:

      var aps = Array.prototype.slice;

      En Javascript, los objetos y metodos nativos del lenguaje se pueden extender (y hasta sobreescribir) como si los hubiéramos creado nosotros mismos. Para eso, utilizamos el método prototype. De hecho, en JS, no existen las clases como en el resto de lenguajes orientados a objetos, sino que trabajamos con prototipos. Pero en este caso, no vamos a sobreescribir nada, solo guardar la referencia.

      Por eso, la línea anterior, lo que hace es cachear el método nativo ‘slice’ del objeto Array para utilizarlo más adelante: es algo así como un shortcut.

      args = aps.call( arguments );

      Lo que hacemos ahora es llamar a la función nativa JS ‘call’ sobre la caché anterior, es decir, sobre el método nativo ‘slice’ del objeto Array.

      El método ‘call’ nos permite invocar al constructor de una clase (prototipo), pasándole el objeto que queremos instanciar y sus atributos.
      Puedes consultar un buen artículo sobre este método y su gemelo ‘apply’ en:

      http://trephine.org/t/index.php?title=JavaScript_call_and_apply

      Finalmente, lo que conseguimos entre ambos (slice y call) es crear un array nativo con aquellos elementos que pasamos como parámetros. En este caso, dado que el argumento es un objeto que funciona como un Array, con slice vamos ‘desligando’ uno a uno los valores que ‘call’ vuelve a pegar. Es lo que denominamos herencia prototípica en Javascript: convertimos un objeto que ‘parece’ un Array en un Array genuino utilizando su prototipo.

      Espero haberte aclarado algo al respecto.
      Un saludo!

  2. Mikel

    Hola!
    Simplemente comentarte un pequeño desliz que existe en el primer bloque de código, concretamente donde pone:

    console.log( testAccess( ‘foo’, ‘bar’, null, [ 1, 2, 3 ] ) );
    // The first arg is: bar, and the last is: 1,2,3

    Creo que lo correcto sería «// The first arg is: foo, and the last is: 1,2,3».

    Un saludo!

    • Carlos Benítez

      Hola Mikel; efectivamente, había una errata.

      Ya está corregido. Muchas gracias!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *