¿Qué son las promesas y generadores?

Hasta la llegada de la nueva API en ES2015 (La nueva versión de Javascript), si queríamos usar de la programación asíncrona teníamos que hacerlo a través de Callbacks, lo que implicaba una serie de problemas:

  • Inversion del control
  • Confusion de los inputs con los outputs
  • Errores más complejos de manejar
  • Código se vuelve difícil seguir leer y mantener

Si profundizamos en estos problemas nos encontramos callbacks que dependen de la salida de otros callbacks, "Callback hell" o el infierno de los callbacks :D. Vamos a entender mejor este concepto con un ejemplo, imaginemos que queremos programar los pasos para crear una pizza. El objetivo de este programilla es saber el tiempo de preparación de una pizza.

// Función que devuelve una función que simula un comportamiento asincrono
const _cookService = function(){  
  return function(callback){
    // Generamos un número entre 1 y 5 para simular un código asincrono
    const timeSpend = (Math.random() * 5) + 1;
    setTimeout( function(){ 
        callback(timeSpend);
    }, timeSpend * 1000 );
  };
};

// Creamos los pasos a través de la función anterior
cookService = {  
  prepararMasa: _cookService(),
  reposarMasa: _cookService(),
  estirarMasa: _cookService(),
  anadirIngredientes: _cookService(),
  hornear: _cookService()
};

console.log( 'Empezando la preparación de la pizza' );  
console.log( '  Preparamos la masa …' );  
cookService.prepararMasa(function(a) {  
  console.log( '  Dejamos reposar la masa …' );
  cookService.reposarMasa(function(b) {
    console.log( '  Estiramos la masa …' );
    cookService.estirarMasa(function(c) {
      console.log( '  Agregamos los ingredientes, extra de queso :3 …' );
      cookService.anadirIngredientes(function(d) {
        console.log( '  Horneando …' );
        cookService.hornear(function(e) {
          var totalTimeSpend = a+b+c+d+e;
          console.log( "Pizza lista en " + totalTimeSpend + " seg, ¡A COMER! :)" );
        });
      });
    });
  });
}); 

Como necesitamos obtener  el tiempo final de cocinado tenemos que ir encadenando callbacks para poder ir obteniendo los tiempos de cada paso y poder realizar la suma total.

cookService.prepararMasa(function(a) {  
  cookService.reposarMasa(function(b) {
    cookService.estirarMasa(function(c) {
      cookService.anadirIngredientes(function(d) {
        cookService.hornear(function(e) {
          var totalTimeSpend = a+b+c+d+e+f;
          console.log( "Pizza lista en " + totalTimeSpend + " seg, ¡A COMER! :)" );
        });
      });
    });
  });
}); 

Aún eliminando los comentarios, vemos que el código es bastante lioso de seguir y si queremos agregar un nuevo paso intermedio sería engorroso. Os dejo el código en JS Bin En este artículo vamos a profundizar en los nuevos modelos para realizar programación asíncrona Modelo de promesas y Modelo de generadores

¿Qué es una Promesa?

Es un objeto que indica el compromiso de entregar una respuesta, resultante
de una llamada asíncrona.

// Creamos una promesa
var promise = new Promise(

    function(resolve, reject) {
      // Simulamos un código asincrono
      window.setTimeout(
        function() {

          // Boolean aleatorio, para simular que la promesa no se resuelve siempre
          var randomBoolean = Math.random() >= 0.5;

          if (randomBoolean) {
            // Se resuelve la promesa
            resolve("Promesa Resuelta!");
          } else {
            // Si la promesa no se resolviese usaríamos
            // También podríamos devolver un objeto Error()
            resolve("Promesa Rechazada!");
          }
        }, Math.random() * 2000 + 1000);
    });

promise  
  .then(function(data){
     console.log("Correcto: " + data);
  })
  .catch(function(err){
     console.log("Error: " + err);
  });

En este ejemplo vemos como es la creación de una promesa, como ejecutarla y recibir la respuesta o el error, os dejo el código en JS Bin

Cargando imagen dinamicamente con promesas

Ahora que nos vamos familiarizando con el tema en este ejemplo voy a usar la sintaxis de ES2015, como podemos ver la sintaxis de =>, hace un código más limpio y claro.

// ES2015
const loadImage = (src) => {  
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = src;
    img.onload = () => resolve(img);
  });
};

// Uso como ejemplo mi imagen de twitter
loadImage("https://pbs.twimg.com/profile_images/690292020762533889/uoHuCXbd.jpg").then(  
  // Uso la sintaxis de la "flecha gorda" para crear la función de callback
  image => document.getElementById("wrapper").appendChild(image)
);

En este código carga una imagen de manera asíncrona en cuanto se resuelve la promesa, os dejo el código en JS Bin

Encadenando promesas

Y por último vamos a retomar el ejemplo de como crear una pizza

const _cookService = function(){  
  return function(){
    // Generamos un número entre 1 y 5 para simular un código asincrono
    const timeSpend = (Math.random() * 5) + 1;
    return new Promise( function( resolve, reject ){
      setTimeout( resolve.bind( resolve, timeSpend ), timeSpend * 1000 );
    });
  };
};

cookService = {  
  prepararMasa: _cookService(),
  reposarMasa: _cookService(),
  estirarMasa: _cookService(),
  anadirIngredientes: _cookService(),
  hornear: _cookService()
};

let totalTimeSpend = 0;  
console.log( 'Empezando la preparación de la pizza' );  
console.log( '  Preparamos la masa …' );  
cookService.prepararMasa()  
  .then( (time) => {
    totalTimeSpend += time;
    console.log( '  Dejamos reposar la masa …' );
    return cookService.reposarMasa();
  } )
  .then( (time) => {
    totalTimeSpend += time;
    console.log( '  Estiramos la masa …' );
    return cookService.estirarMasa();
  } )
  .then( (time) => {
    totalTimeSpend += time;
    console.log( '  Agregamos los ingredientes, extra de queso :3 …' );
    return cookService.anadirIngredientes();
  } )
  .then( (time) => {
    totalTimeSpend += time;
    console.log( '  Horneando …' );
    return cookService.hornear();
  } )
  .then( (time) => {
    totalTimeSpend += time;
    console.log( "Pizza lista en " + totalTimeSpend + " seg, ¡A COMER! :)" );
  } );

El funcionamiento es el siguiente:

  • Creamos las promesas para cada paso de la preparación de la pizza
  • Y empezamos a encadenar el resultado de las promesas

Os dejo el código en JS Bin

Lo bueno y lo malo de usar promesas

El uso de promesas nos ofrece grandes beneficios, aunque su uso puede llegar a ser artificial e incomodo de tratar Ventajas

  • Recuperamos el return y la asignación
  • APIs más limpias sin métodos de callback (callback en cliente)
  • Estructura del programa más similar a la programación secuencial
  • Razonamos con promesas como valores de futuro

Inconvenientes

  • No deja de ser necesario inyectar funciones manejadoras de éxito y error
  • Resulta difícil depurar hasta que las promesas no se han resuelto
  • Resulta más invasivo generar APIs que consuman y generen promesas

¿Que es un generador?

Un generador es una función que puede ser interrumpida y retomada en cualquier momento, al pausarlo el programa sigue su curso, no bloquea la ejecución. La función generadora no se ejecuta inmediatamente, esta función se define de la siguiente forma:

function* nombre([param[, param[, ... param]]]) {  
   instrucciones
}

La función devuelve un objeto iterador, y cuando llamamos al next() del iterador, se devuelve el valor del primer yield. *¿Qué es un iterador? *Un iterador es objeto que es iterable (valga la redundancia :D), es decir una colección que se puede recorrer y mantiene la referencia a la posición actual.

// Usamos la sintaxis function* para declarar un generador
// yield, devuelve el valor indicado a su derecha
function* generador() {  
    yield 1;
    yield 2;
    yield 3;
}

// Recibimos un objeto iterador
var iterador = generador();

// next() ejecuta el iterador hasta el siguiente yield
// dentro del mismo y retorna un objeto con el valor a la derecha de este
console.log(iterador.next()); // { value: 1, done: false }  
console.log(iterador.next()); // { value: 2, done: false }  
console.log(iterador.next()); // { value: 3, done: false }  
console.log(iterador.next()); // { value: undefined , done: true }  

En este ejemplo muy sencillo entendemos mejor su funcionamiento, os dejo el ejemplo en JS Bin 

function* generador() {  
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return "Fin";
}

var iterador = generador();

console.log(iterador.next()); // { value: 1, done: false }  
console.log(iterador.next()); // { value: 2, done: false }  
console.log(iterador.next()); // { value: 3, done: false }  
console.log(iterador.next()); // { value: 4, done: false }  
console.log(iterador.next()); // { value: 5, done: false }  
console.log(iterador.next()); // { value: Fin , done: true }  
// A partir de aquí se pierde el valor
console.log(iterador.next()); // { value: undefined , done: true }

// 
iterador = generador();

// Podemos usar loop para recorrer el generador
for (var value of iterador) {  
  console.log( value ); // Solo recorre el iterador hasta el 5
}

Como vemos es posible recorrer el generador con loops, os dejo el ejemplo en JS Bin

function* generador(x) {  
    var y = 2 + (yield (x + 1)); // Primer yield
    var z = yield (y + 3); // Segundo yield
    return (x + y + z);
};

var iterador = generador( 15 ); // x es 15

console.log( iterador.next() ); // Devuelve 16 yield=(15 + 1)

// Sustituye 12 como valor del yield, y=(2 + 12) yield=(y + 3) y el valor es 17
console.log( iterador.next(12) );

// Sustituye 13 en el segundo yield
// Devuelve 42 ya que (x=15 + y=(2+12) + z=13)
console.log( iterador.next(13) );  

Un pequeño ejemplo del paso de variables a los generadores, os dejo el ejemplo en JS Bin

// Un iterador puede contener otro iterador
function* anotherGenerator(i) {  
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function* generator(i){  
  yield i;
  yield* anotherGenerator(i);
  yield i + 10;
}

var gen = generator(10);

console.log(gen.next().value); // 10  
console.log(gen.next().value); // 11  
console.log(gen.next().value); // 12  
console.log(gen.next().value); // 13  
console.log(gen.next().value); // 20  

Y por último un fragmento en el que un iterador delega en otro, para ello se hace uso de yield*, os dejo el ejemplo en JS Bin Si retomamos el ejemplo de la preparación de la pizza con generadores conseguimos un código más expresivo:

import co from "co"; 

const _cookService = function(){  
  return function(){
    // Generamos un número entre 1 y 5 para simular un código asincrono
    const timeSpend = (Math.random() * 5) + 1;
    return new Promise( function( resolve, reject ){
      setTimeout( resolve.bind( resolve, timeSpend ), timeSpend * 1000 );
    });
  };
};

cookService = {  
  prepararMasa: _cookService(),
  reposarMasa: _cookService(),
  estirarMasa: _cookService(),
  anadirIngredientes: _cookService(),
  hornear: _cookService()
};

// Creamos el generador y tratamos cada promesa como si fuera una funcion sincrona
// Envolvemos nuestro generador con una función en este caso se usa co, que controla el flujo de control
co(function* (){  
  let duracionCocinado = 0;

  duracionCocinado += yield cookService.prepararMasa();
  duracionCocinado += yield cookService.reposarMasa();
  duracionCocinado += yield cookService.estirarMasa();
  duracionCocinado += yield cookService.anadirIngredientes();
  duracionCocinado += yield cookService.hornear();

  return duracionCocinado;
}).then( (duracionCocinado) => console.log( "Pizza lista en " + totalTimeSpend + " seg, ¡A COMER! :)" ) );

Como vemos tenemos que hacer uso de una librería externa CO, que es una función que recibe un generador y gestiona las llamadas a la secuencia de promesas, cuando se completan todas, se devuelve otra promesa con el resultado final.

Lo bueno y lo malo de usar generadores

Aunque los generadores acercan la programación asíncrona a una experiencia de desarrollo mas secuencial, puede llegar a ser engorroso el uso de cláusulas yield para realizar el manejo del control del flujo. Ventajas

  • Esquema de programación similar al secuencial
  • Transparencia en los procesos de gestión de continuadores
  • Curva de aprendizaje corta. Con pocas reglas de pulgar se puede razonar fácilmente en el modelo

Inconvenientes

  • Artificialidad del código. Perversión del uso de los yields
  • Todo código esta siempre dentro de un contexto Co
  • El código está plagado de yields
  • ¿Qué está haciendo Co por debajo?