En javascript cuando llamamos a una función esta se ejecuta de inicio a final.
function logger() {
console.log('Start');
console.log('End');
}
logger();
Las funciones generadores de ES6 permite pausar la ejecución de una función y reanudarla (añadimos un “*” a nuestra función).
- function* logger() {
+ function* logger() {
console.log('Start');
console.log('End');
}
logger();
https://codepen.io/Lemoncode/pen/gQdzON?editors=0012
Si ejecutamos esto, no tenemos resultados, esto es porque cuando llamamos a un generador devuelve una instancia de si mismo.
Renombramos la función a createLogger y llamando a next la podemos ejecutar.
- function* logger() {
+ function* createLogger() {
console.log('Start');
console.log('End');
}
- logger();
+ const logger = CreateLogger();
+ logger.next();
https://codepen.io/Lemoncode/pen/WYgJjG?editors=0001
Ahora si ejecutamos vemos como la función de se ejecuta de principio a fin.
¿Cómo podemos pausar la ejecución de nuestra función generadora? Usando el comando yield
function* CreateLogger() {
console.log('Start');
+ yield;
console.log('End');
}
const logger = CreateLogger();
logger.next();
Si ejecutamos veremos que sólo vemos start en nuestra consola.
https://codepen.io/Lemoncode/pen/pQOVrw?editors=0011
Para poder llegar hasta el final de la función tendríamos que volver a ejecutar logger.next
function* CreateLogger() {
console.log('Start');
yield;
console.log('End');
}
const logger = CreateLogger();
logger.next();
+ logger.next();
Podemos añadir tantos yields como queramos (tendremos que llamar tantos next como yields tengamos).
Cuando tenemos un generador y una instancia del generador podemos enviar mensajes desde el generador a la instancia y viceversa.
Vamos a probar a enviar un mensaje desde el generador a la instancia activa:
Updated function name !*
function* createHello() {
yield 'first'
}
const hello = createHello();
console.log(hello.next());
Cuando ejecutamos se muestra por consola la siguiente salida:
{value: 'first', done: false}
¿Pero se ejecuta la función hasta al final? Vamos a verlo:
function* createHello() {
console.log('before yield');
yield 'first';
console.log('after yield');
}
const hello = createHello();
console.log(hello.next());
En valor tenemos el valor que yield devolvío, y la salida done nos indica que la función generador no ha terminado aún.
Para terminar de ejecutar nuestra llamada al generador basta con hacer otra llamada a next
function* createHello() {
yield 'first'
}
const hello = createHello();
console.log(hello.next);
+ console.log(hello.next);
Está segunda llamada nos muestra la siguiente salida por consola:
{value: undefined, done: true}
Es decir no hay valor devuelto por yield (hemos terminado de ejecutar la función, y done está a true, indicandos que nuestra generador ha terminado con la ejecución.
También podemos pasar información desde nuestra instancia del generador al generador, veamos como:
function* createHello() {
const word = yield;
console.log(word);
}
const hello = createHello();
console.log(hello.next());
console.log(hello.next('Max'));
Veamos lo que obtenemos por consola:
{value: undefined, done: false}
Max
{value: undefined, done: true}
Vamos a ver como controlar errores desde un generador.
Cuando invocamos a un generador parece que tenemos un código síncrono, y en realidad estamos ejecutando un flujo asínncono, es decir entre un next y otro next pueden ocurrir mil cosas.
function* createHello() {
const word = yield;
console.log(word);
}
const hello = createHello();
console.log(hello.next());
// ... Many operations (async) before resuming our generation
console.log(hello.next('Max'));
Si algo va mal en algo en el código que estamos haciendo, ¿ Cómo podemos informar al generador de que ha ocurrido un error? Podemos lanzar un throw desde fuera, y en el generador capurar la excepción con un try catch
function* createHello() {
+ try {
const word = yield;
console.log(word);
+ }
+ catch(err) {
+ console.log(`Error: ${err}`);
+ }
}
const hello = createHello();
console.log(hello.next());
// ... Many operations (async) before resuming our generation
+ hello.throw('Fatal error...');
console.log(hello.next('Max'));
https://codepen.io/Lemoncode/pen/gQdKyv?editors=0012
- Vamos a ver ahora como iterar en un generador, para ello es6 nos provee del bucle for of que nos permite iterar por un generador hasta que termina done, un ejemplo de esto:
function * createCounter() {
yield "Hello from step 1";
yield "Hello from step 2";
yield "Hello from step 3";
yield "Hello from step 4";
}
const counter = createCounter();
for(let message of counter) {
console.log(message);
}
Esto nos genera la siguiente salida por consola:
Hello from step 1
Hello from step 2
Hello from step 3
Hello from step 4
https://codepen.io/Lemoncode/pen/vQzaBe?editors=0010
¿ Funcionaría map _reduce? No, tenemos que hacernos el nuestro: https://stackoverflow.com/questions/45983883/mapping-a-function-on-a-generator-in-javascript un ejemplo: https://dev.to/nestedsoftware/lazy-evaluation-in-javascript-with-generators-map-filter-and-reduce--36h5
- También podemos tener generadores anidados, por ejemplo si tenemos:
function* create3To4Counter() {
yield 3;
yield 4;
}
function* createCounter() {
yield 1;
yield 2;
yield 5;
}
for(let i of createCounter()) {
console.log(i);
}
Y queremos interacalar la ejecuión del primero generador dentro del segundo, entre le paso 2 y el 5 sólo tenemos que introducir la siguiente linea de código:
function* create3To4Counter() {
yield 3;
yield 4;
}
function* createCounter() {
yield 1;
yield 2;
+ yield* create3To4Counter();
yield 5;
}
for(let i of createCounter()) {
console.log(i);
}
Esto genera la siguiente salida por la consola del navegador:
1
2
3
4
5
Otro tema interesante es que podemos hacer un return de un valor en el primer generador y que lo coja el segundo.
function* create3To4Counter() {
yield 3;
- yield 4;
+ return 4;
}
function* createCounter() {
yield 1;
yield 2;
const four = yield* create3To4Counter();
console.log(four);
yield 5;
}
for(let i of createCounter()) {
console.log(i);
}
Esto nos daría la misma salida por consola que el paso anterior.