domingo, 10 de febrero de 2019

TypeScript: Funciones de flecha

Las funciones flecha son una de las novedades ES6 (ECMAScript 2015) que más pueden desconcertar a los desarrolladores tradicionales. Su estructura intimida a simple vista y, de hecho, cuando nos las encontramos en un código sin avisar, pueden parecer incluso un error de sintaxis. Quizá ya os ha pasado: revisáis un código moderno en un blog, o en un repositorio de GitHub, e inmediatamente se os va la vista a una estructura rara que no parece que vaya a funcionar en la vida. -¿Pero eso es Javascript? – sería otra pregunta recurrente que quizá os haya hecho algún colega mientras jugáis con esta nueva sintaxis a escondidas…
Pero como suele ocurrir, no son algo radicalmente nuevo: los programadores que conozcan por ejemplo la sintaxis de CoffeeScript se sentirán como en casa; es una estructura natural que resulta cómoda, menos redundante que la tradicional, y que de paso pretende solucionar los malentendidos que históricamente ha generado el maldito valor de this en Javascript.

Dado que es un tema complejo que puede dar mucho juego, vamos a dedicarle un par de entradas para estudiarlo. Analizaremos su sentido, estructura, finalidad, compatibilidad y limitaciones…

¡Vamos a ello!

Un vistazo rápido: su sintaxis.

La sintaxis de estas funciones es, como mínimo, exótica. Como comenté más arriba, parecen más un error de sintaxis que una estructura válida:

( param1, param2, ...rest ) => { expression; }

Eso sería la definición formal, pero como así no nos enteramos de nada, vamos a tomar un ejemplo en código real:

var add = ( x, y ) => x + y;

Bueno… quizá así tampoco lo veamos claro, Pero solo es cuestión de familiarizarse con esta estructura… básicamente, esa función de ahí arriba (una función pura por cierto), devuelve la suma de los parámetros dados:

console.info( add( 2, 4 ) ); // 6
console.info( add( 4, 4 ) ); // 8
console.info( add( 1, 2 ) ); // 3

Comparemos nuestra función de tipo flecha con su correspondecia en el Javascript más estándar/clásico:

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

Desde un punto de vista de la sintaxis, en nuestra función tradicional tenemos un identificador (un nombre), una recogida de parámetros, un cuerpo y su argumento de retorno. Con este dibujito podemos identificarlo todo gráficamente:

flecha-1

Con las funciones flecha, buscamos simplificar todo lo anterior de un modo mucho más directo y declarativo:

- Eliminamos la palabra reservada function y nos limitamos a recoger los parámetros mediante los paréntesis tradicionales.
- Podemos eliminar las llaves que delimitan el scope abriéndolo con una flecha.
- Podemos eliminar la palabra reservada return.

NOTA: Cuando me refiero a ‘function’ o ‘return’, estoy evitando usar la palabra ‘comando’ porque cada vez que la veo en castellano, se me ponen los pelos de punta. Es una mala traducción (como tantas otras) del inglés command. Sería más apropiado el término ‘instrucción’, pero como tampoco me convence, pues nada: palabra reservada.

Si analizamos la función flecha con otro dibujo, se verán mejor las correspondencias:

flecha-2

Algunas cosas que saltan a la vista:

- Las funciones flecha son siempre anónimas. Estamos utilizando una estructura declarativa donde asignamos la función a una variable. Eso nos permite su reutilación, pero por definición, estas funciones son anónimas.

- La sintaxis es mucho más limpia y simple. Superado el shock inicial de su estructura, rápidamente nos acostumbramos y la forma tradicional comienza a parecer verborreica (sí, el tan frecuente verbose anglosajón existe en castellano, ¿por qué no usarlo?).

Preguntas rápidas que se nos echan encima.

- ¿Qué ocurre si solo tenemos un parámetro, o ninguno, o infinitos?
- ¿Qué ocurre si nuestra función necesita realizar varias operaciones? ¿Cómo desarrollo el cuerpo si no he visto llaves? ¿Cómo indico el valor de retorno?
- Se ha mencionado más arriba pero, ¿qué ha cambiado en el this?

Vamos a ir viendo ejemplos…

Si solo tenemos un parámetro, podemos obviar el paréntesis:

var double = x => x * 2;
 
console.info( double( 2 ) ); // 4
console.info( double( 5 ) ); // 10

Si no necesitamos parámetros, tenemos que incluir el paréntesis vacío:

var hi = () => 'Hello World';
 
console.info( hi() ); // Hello World

Si necesitamos una entrada más compleja, un aspecto importante es que el objeto arguments no funciona:

var test = () => arguments;
 
console.info( test( 'foo', 'bar' ) ); // ERROR: arguments is not defined

Sin embargo, el operador de arrastre sí lo hace correctamente:

var test = ( ...args ) => args;
 
console.info( test( 'foo', 'bar' ) ); // [ 'foo', 'bar' ]

Si necesitamos incluir varias instrucciones en nuestra función, entonces necesitamos incluir las clásicas llaves y el retorno:

var foo = ( param1, param2 ) => {
    var result;
 
    // Do amazings things here...
 
    return result;
};

NOTA: Ojo con las llaves. Un error de sintaxis frecuente es olvidar la flecha delante.

Un ejercicio práctico de refactorizado.

Ahora que más o menos conocemos la estructura, veamos cómo convertir una función clásica a esta nueva sintaxis. Para no perdernos de golpe, lo haremos de forma progresiva.

Cojamos por ejemplo esta función que calcula la media aritmética de los valores de una matriz (un array) dado.

function getAvg( ...values ) {
    return values.reduce( function ( p, c ) {
        return p + c;
    } ) / values.length;
}
 
getAvg( 3, 7 ); // 5
getAvg( 2, 10 ); // 6

La idea es simple: sumamos todos los valores y los dividimos por el número total de ellos. En el cole, esto nos habría venido bien para saber nuestras notas finales.

Pasemos ahora a la conversión. Tenemos aquí una función dentro de otra, lo que permite mucho juego. Eliminemos por ejemplo la primera parte:

var getAvg = ( ...values ) => {
    return values.reduce( function ( p, c ) {
        return p + c;
    } ) / values.length;
}

Genial, hemos eliminado la instrucción ‘function’ pero dejado el ‘return’ de momento. Ahora eliminemos la segunda función:

var getAvg = ( ...values ) => {
    return values.reduce( ( p, c ) => p + c ) / values.length;
}

Esto se va quedando limpito: la segunda nos ha permitido eliminar tanto la instrucción ‘function’ como su las llaves y el ‘return’.

Vamos a por otro paso eliminando el primer ‘return’:

var getAvg = ( ...values ) => values.reduce( ( p, c ) => p + c ) / values.length;

Esto está mucho mejor, pero es una línea muy larga. Javascript nos permite introducir un salto de línea tras las flechas, lo que facilita un código indentado más legible:

var getAvg = ( ...values ) =>
    values.reduce(
        ( p, c ) => p + c
    ) / values.length;

El primer paréntesis es necesario por el hecho de que para recoger los valores, estamos usando el operador de arrastre (…values). Si fuese un parámetro normal, como una matriz directamente, podríamos eliminarlo. Quedaría así:

var getAvg = arr =>
    arr.reduce(
        ( p, c ) => p + c
    ) / arr.length;
 
console.info( getAvg( [ 2, 8 ] ) );

La diferencia en este caso es que como entrada, necesitamos ya indicar un ‘array’.

Realizar este tipo de ejercicios de refactorizado es una excelente manera de que nos acostumbremos a utilizar estas funciones.

El objeto this.

Ya hablamos en su día de uno de los problemas más delicados del lenguaje: el valor de this.

Como repaso rápido que sirva de introducción, este problema (considerado por Douglas Crockford como un bug del lenguaje) implica que el valor referencial de this se asocia a cada uno de los ámbitos/scopes desde el que se invoca, lo cual rompe en parte la teoría general de la Programación Orientada a Objetos:

function Suffixer( suffix ) {
    this.suffix = suffix; // Definition
}
 
Suffixer.prototype.suffixArray = function ( arr ) { // (A)
    'use strict';
 
    return arr.map( function ( x ) { // (B)
        // Doesn’t work:
        return x + this.suffix; // (C)
    } );
};
 
var suff = new Suffixer( '.' + new Date().getTime() );
 
suff.suffixArray( [ 'avatarPic', 'userPic', 'otherImage' ] );

En ese ejemplo anterior, la idea es crear una función que permita añadir sufijos a cada elemento de un array (algo útil por ejemplo para evitar colisiones al guardar nombres de ficheros en base de datos). Sin embargo, el problema lo tenemos en la línea marcada como ‘C’: ahí, this debería estár haciendo referencia al contexto del constructor y, por tanto, acceder al valor asignado en ‘Definition’. Sin embargo, el resultado es un error del intérprete donde se nos avisa de que ‘this is undefined’.

A lo largo de los años, hemos tenido que lidiar con este comportamiento utilizando diversas técnicas: cacheando el valor de this, sobreescribiendo su valor en tiempo de ejecución o utilizando el método bind. Cualquiera de estas formas devenía en un código poco elegante e intuitivo, muy propenso a errores posteriores en posibles refactorizados.

Con las nuevas funciones flecha, esta asociación contextual del valor de this se produce de forma nativa:

Suffixer.prototype.suffixArray = function ( arr ) { // (A)
    'use strict';
 
    return arr.map( x => x + this.suffix );
};

Hemos transfomado la función dentro de map convirtiéndola a la nueva sintaxis. Aprovechamos para repasar: hemos eliminamos la instrucción ‘function‘, suprimido los paréntesis porque solo hay necesitamos un parámetro/argumento, eliminado las llaves del contexto y omitido la palabra reservada ‘return‘.

NOTA: También hemos eliminado el indicador de modo estricto ya que, dentro de este marco contextual, el intérprete simplemente ignora la instrucción.

Si ejecutamos ahora nuestro código, podremos ver que el resultado es el esperado:

suff.suffixArray( [ 'avatarPic', 'userPic', 'otherImage' ] );
// [ "avatarPic.1466594576838", "userPic.1466594576838", "otherImage.1466594576838" ]

¡¡OJO CON LOS CONSTRUCTORES!!

Si observamos el ejemplo anterior, vemos que el constructor Suffixer podría ser un buen candidato para refactorizar. Si nos metemos con ello, obtendríamos el siguiente código:

var Suffixer = suffix => { this.suffix = suffix; };
 
Suffixer.prototype.suffixArray = arr =>
    arr.map( x => x + this.suffix );

Sin embargo, si probamos a ejecutar nuestro ejercicio, obtendremos un error de sintaxis:

var suff = new Suffixer( '.' + new Date().getTime() );
 
suff.suffixArray( [ 'avatarPic', 'userPic', 'otherImage' ] );
// TypeError: Suffixer.prototype is undefined

Y aquí tenemos una regla importante que no podemos olvidar: las funciones flecha no pueden ser utilizadas como constructores.

Retorno de objetos literales.

Otro punto a tener en cuenta es que si pretendemos devolver un objeto literal, la sintaxis no es quizá la más intuitiva:

var getVersion = () => { version: '1.0.0' };
 
console.info( getVersion() ); // undefined

Si arriba, con solo un registro de clave/valor tenemos un resultado inesperado, con más de uno la cosa es peor:

var getSettings = () => {
    baseURL: 'https://openlibra.com',
    appName: 'OpenLibra',
    version: '1.0.0'
};
 
// SyntaxError: missing ; before statement

La cuestión aquí es que, para devolver un objeto de forma literal, la sintaxis de las funciones flecha exige el uso de paréntesis alrededor de las llaves:

var getVersion = () => ( { version: '1.0.0' } );
 
console.info( getVersion() ); // Object { version="1.0.0" }

Para el segundo ejemplo:

var getSettings = () => ( {
    baseURL: 'https://openlibra.com',
    appName: 'OpenLibra',
    version: '1.0.0'
} );
 
console.info( getSettings() );
// Object { baseURL= "https://openlibra.com", appName="OpenLibra", version="1.0.0" }

La explicación de esta necesidad de paréntesis es que, en ausencia de ellas, el bloque de llaves se interpreta como una secuencia de sentencias. Eso quiere decir que para Javascript, el código sería similar al que vimos en su día para la definición de bloques:

function test ( param1, param2 ) {
 
    declareVars: {
        var myConstant = 100,
        result;
    }
 
    processing: {
        result = myConstant + param1 + param2;
    }
 
    printResults: {
        return result;
    }
 
}

En definitiva, los bloques que utilizamos para organizar el código en el cuerpo de una función tradicional, se puede convertir en un posible error de sintaxis en las nuevas estructuras de tipo flecha si no tenemos cuidado.

Indentado y organización.

Dado el formato de estas funciones, en ocasiones podemos terminar con líneas de código demasiado largas. Por ejemplo, en un tratamiento de promesas podríamos ver algo similar a esto:

asyncFunction().then( () => responseA() ).then( () => responseB() ).done( () => finish );

Para reforzar la legibilidad, la siguiente tabla muestra dónde el intérprete nos permite introducir saltos de línea:

var func1 = ( x, y ) // SyntaxError
=> {
    return x + y;
};
 
var func2 = ( x, y ) => // OK
{
    return x + y;
};
 
var func3 = ( x, y ) => { // OK
    return x + y;
};
 
var func4 = ( x, y ) // SyntaxError
=> x + y;
 
var func5 = ( x, y ) => // OK
x + y;
 
var func6 = ( // OK
    x,
    y
) => {
    return x + y;
};

Invocación inmediata de funciones flecha.

Al igual que ocurre las funciones tradicionales autoejecutables –IIFE-, las funciones flecha también permiten su ejecución inmediata –IIAF– (Immediately-invoked arrow functions):

( () => {
    return 'Hello World';
} ) ();

Esa construcción que quizá recuerda con cierta nostalgia al lenguaje LISP, permite todo el juego de malabares propio que llevamos utilizando desde hace una década. Por ejemplo, podríamos implementar el patrón del módulo extendido jugando con los parámetros/argumentos.

Primero, la versión ‘clásica’:

// file: core.js
var MODULE = ( function () {
    var my = {},
        privateVariable = 1;
 
    function privateMethod () {
        // ...
    }
 
    my.moduleMethod = function () {
        console.info( 'Method from core...' );
    };
 
    return my;
} () );
 
// file: plugin.js
var MODULE = ( function ( my ) {
    my.anotherMethod = function () {
        console.info( 'Method from plugin...' );
    };
 
    return my;
} ( MODULE || {} ) );
 
console.info( MODULE.moduleMethod() ); // Method from core...
console.info( MODULE.anotherMethod() ); // Method from plugin...

Y, ahora, la versión con flechas:

// file: core.js
var MODULE = ( () => {
    var my = {},
        privateVariable = 1;
 
    var privateMethod = () => {
        // ...
    };
 
    my.moduleMethod = () => console.info( 'Method from core...' );
 
    return my;
} ) ();
 
 
// file: plugin.js
var MODULE = ( ( my ) => {
    my.anotherMethod = () => console.info( 'Method from plugin...' );
 
    return my;
} ) ( MODULE || {} );
 
console.info( MODULE.moduleMethod() ); // Method from core...
console.info( MODULE.anotherMethod() ); // Method from plugin...

NOTA: De nuevo, animo a que probéis a realizar este tipo de refactorizados para ir practicando con la sintaxis de estas nuevas funciones.

Patrones más complejos: aplicaciones parciales y el currying.

Ya expliqué en su día el concepto del currying en Javascript, incluso lo volvimos a tocar de pasada cuando nos referimos a las Funciones Puras al hablar sobre el beneficio de éstas con la Memoization.

La idea principal proviene de la Programación Funcional y se define como el proceso de fijar valores a los argumentos de una función antes de que sea invocada.

Retomemos nuestro ejemplo más simple de función pura -que también hemos utilizado aquí para hablar de la sintaxis de las funciones flecha-:

function add ( x, y ) {
    return x + y;
}

O su versión en la nueva sintaxis:

var add = ( x, y ) => x + y;

Si necesitamos crear una aplicación parcial a partir del fragmento anterior para, por ejemplo, crear una nueva función que sume 1 a un número dado, podríamos escribir lo siguiente:

var plus1 = y => add( 1, y );
 
console.info( plus1( 10 ) ); // 11
console.info( plus1( 7 ) ); // 8

Como se puede deducir del código anterior, las posibilidades son muy interesantes cuando buscamos la reutilización del código y la diversificación de cómputos.

¿Y el currying?

Pues podríamos re plantearlo usando el clásico ejemplo de la función incrementadora. Si tenemos:

function writeSeq ( start, end ) {
    for( var i = start; i <= end; ++i ) {
        console.log( i );
    }
}

O mejor su versión en modo flecha:

var writeSeq = ( start, end ) => {
    for( var i = start; i <= end; ++i ) {
        console.log( i );
    }
}

Podemos implementar un currying de forma simple con la mínima expresión de esta funcionalidad:

var curry = ( fn, ...args1 ) => ( ...args2 ) => fn( ...args1, ...args2 );

O si queremos indentarla para que no sea tan larga:

var curry = ( fn, ...args1 ) =>
    ( ...args2 ) =>
        fn( ...args1, ...args2 );

Esa ‘simple’ línea de código nos permite ahora fijar el valor para funciones derivadas:

var seq20 = curry( writeSeq, 20 );
seq20( 25 ); // 20, 21, 22, 23, 24

Realmente simple; el poder combinado de esta sintaxis junto con el del parámetro de arrastre, permite reducir mucho el código fuente…

Aplicaciones parciales.

Con el método bind, ya hemos visto que a partir del segundo argumento, podemos ir fijando los parámetros de la función que replicamos. Con eso conseguimos implementar las famosas aplicaciones parciales:

var sum = function ( x, y ) {
    return x + y;
}
 
var plus10 = sum.bind( undefined, 10 );
 
console.info( plus10( 20 ) ); // 30

NOTA: Recordad que el primer parámetro de bind es el que asigna el valor referencial de this. Como en este caso no nos interesa modificarlo, utilizamos undefined.

Con las Funciones Flecha, la sintaxis es mucho más clara:

var plus10 = y => sum( 10, y );
 
console.info( plus10( 30 ) ); // 40

Asociación.

Si queremos asociar una función anónima a un callback, con bind escribiríamos un código como el siguiente:

var logger = {
    clickCounter: 0,
    addClick: function (){
        console.info( ++this.clickCounter );
    }
}
 
var ele = $( '#myEle' );
ele.on( 'click', logger.addClick.bind( logger ) );

Y con la sintaxis de flechas, tendríamos:

ele.on( 'click', () => logger.addClick() );

Extracción.

Cuando extraemos un método que debe funcionar como un callback, necesitamos fijar el valor de this para que éste no apunte al objeto global.

Con bind ya comprobamos la estructura necesaria:

var ele = $( '#myEle' );
ele.on( 'click', console.info.bind( console ) );

Veamos ahora la conversión en modo flecha:

ele.on( 'click', x => console.info( x ) );

Y eso es todo.

2 comentarios: