Javascript ES6, generators y promesas

Fintech
En este artículo vamos a obtener una breve introducción acerca de la entrada del estandar ES6 en Node.js. Después, mostraremos un ejemplo práctico de uso de funciones generadoras y la libreria co como una nueva alternativa para el control de flujo asíncrono en ES6.

Al inicio del 2015, la versión estable de Node.js era 0.10. Si querías utilizar nuevas funcionalidades de V8, debías trabajar con la versión inestable 0.11. Ese era el tiempo donde las versiones de Node se mantenian por muchos muchos meses y comenzaron los problemas con Node.js y su fork io.js. Pero el 6 de febrero todo cambio al publicarse la versión 0.12 donde características como block scoping, generator functions y promise objects estaban disponibles mediante el argumento --harmony. Este argumento no debería utilizarse porque las versiones 4.x (LTS) y 5.x vienen con dichas funcionalidades habilitadas por defecto. Sin embargo, si queremos probar funcionalidad casi completada podemos continuar utilizando el argumento --harmony. El argumento --harmony_destructuring no es aconsejable porque habilita funcionalidad del motor V8 que permanece en fase de desarrollo.

ECMAScript 2015 (ES6) actualiza de manera muy notable la sintaxis de muchas operaciones frecuentes con Javascript. Por ejemplo, const, let, arrow functions, Promise, interpolación de strings,… No obstante, la funcionalidad más interesante son las funciones generadoras. Estudiando la definición de este nuevo tipo de funciones, podemos pensar que no son útiles para el trabajo usual, pero más adelante veremos que son una opción válida para manejar el flujo asíncrono de operaciones en nuestra aplicación con Node.js.

Por ejemplo, en el siguiente bloque vemos el clásico caso de callback hell.

function bar(name, callback) {}
function xyz(name, callback) {}
function baz(name, callback) {}

function foo(callback) {
  bar('bar', function(err, barRes) {
    if (err) {
      callback(err);
      return;
    }
    xyz('xyz', function(err, xyzRes) {
      if (err) {
        callback(err);
        return;
      }
      baz('baz', function(err, bazRes) {
        if (err) {
          callback(err);
          return;
        }
        callback(null, [ barRes, xyzRes, bazRes ]);
      });
    });
  });
}

Para mejorar el estilo, podemos usar la libreria Async.js y su método series. También, podemos obtener una promesa con Bluebird (Promise.promisify) de una función que tiene como último argumento una función callback. De esta manera, controlamos vía promesas el flujo asíncrono. Pero en muchas ocasiones, como por ejemplo mongoose, la misma libreria ofrece su API para ser utilizada mediante callbacks o promesas.

Para el uso de funciones generadoras es necesario una libreria de control de flujo, para evitar la gestión del objeto iterador (obtenido tras invocar la función generator) y su método next. co es una de las mejores librerias en este aspecto. Esta librería permite gestionar el flujo asíncrono de diferentes tipos de objetos con la palabra clave yield. Entre estos objetos se encuentran las promesas, funciones generadoras, arrays (por ejemplo, de promesas),… Es muy recomendable leer su documentación y los ejemplos prácticos.

const co = require('co');

function* bar(name) {}
function* xyz(name) {}
function* baz(name) {}

function* foo() {
  const barRes = yield bar('bar');
  const xyzRes = yield xyz('xyz');
  const bazRes = yield baz('baz');
  return [ barRes, xyzRes, bazRes ];
}
co(function*() {
  yield foo();
  console.log('Good-bye');
  process.exit();
}).catch(function(err) {
  console.error('Something bad...', err);
  process.exit(1);
});

En la función foo se ejecutaría de manera secuencial las funciones generadoras bar, xyz y baz. Mientras estas funciones ejecutan su código asíncrono, el event loop de Node.js procesará otras tareas hasta que dichas funciones finalicen.

Otra opción interesante, es ejecutar las tres funciones en paralelo si modificamos el código de la función foo. Para disminuir el tiempo de espera de las funciones del anterior ejemplo, podríamos colocar yield delante de un array con funciones generadoras. De esta manera las tres funciones se ejecutan en paralelo y no esperamos a que termine la anterior para comenzar la siguiente.

function* foo() {
  return yield [
    bar('bar'),
    xyz('xyz'),
    baz('baz'),
  ];
}

La función foo terminará en cuanto termine la última función generadora de la colección, que puede que no sea la última función declarada en el array. Al final, todo depende del flujo asíncrono y del tiempo de respuesta de cada función asíncrona.

En conclusión, las funciones generadoras y librerias como co es una solución muy cómoda para gestionar el flujo asíncrono de una aplicación. Conseguimos escribir código asíncrono de manera síncrona (en el mismo nivel de identación), simplificamos la gestión de errores en una sola función o con el uso de try...catch, forma parte del estandar ES6. Como desventaja, necesitamos una librería que gestione el flujo asíncrono y la falta de librerías que puedan ser utilizadas con funciones generadoras o librerias no tengan su API con promesas o thunks. El rendimiento entre las dos alternativas (callbacks vs promises) es un tema interesante, que da lugar para otro artículo, pero en lineas generales, no hay grandes diferencias.

Otra solución, probablemente más óptima, vendrá con la siguiente versión de Javascript ES7 y las funciones async.

Si te parece interesante esta alternativa, puedes consultar el framework web koa basado con funciones generadoras. Además, el pasado jueves 18 de febrero realicé una pequeña charla en el grupo ValenciaJS sobre el mismo tema, Javascript ES6, funciones generadoras y promesas.

Anuncios