Wextensible

Qué es un generador

Figura
Figura. Código de una función generadora en JavaScript

Un generador es una función cuya ejecución puede ser pausada en un momento dado y continuada en otro posterior. En la pausa, que se produce cuando se ejecuta una expresión con yield, la función conserva su estado de variables. La pausa es, por tanto, ejecutada desde la propia función generadora con yield, mientras que la reanudación sólo puede llevarse a cabo externamente. Esta característica, que podemos denominar como trabajo cooperativo en JavaScript, tiene muchas utilidades.

JavaScript siempre se ejecuta en un único hilo. Y a veces necesitamos que dos cosas se ejecuten de forma concurrente. El Web Worker es una técnica para dotar de cierta concurrencia a JavaScript, aunque la comunicación entre el hilo principal y el del Worker sólo puede realizarse por medio del paso de mensajes. Son dos ejecuciones en dos contextos distintos que no pueden compartir el espacio de variables, por lo que no es una verdadera concurrencia.

Así que por ahora la única forma de hacer dos cosas a la vez es llevar a cabo ejecuciones cooperativas haciendo uso de eventos. Un evento es una acción que se define ahora para ser ejecutada en el futuro. Como un click en un botón o una acción que será llevada a cabo con el reloj del sistema usando window.setTimeout(). Y en esto los generadores pueden ayudarnos mucho.

De esas aplicaciones avanzadas de los generadores hablaremos en los dos temas siguientes. En uno de ellos intentaremos simular multitarea con generadores. En el otro veremos como aplicar generadores a tareas asíncronas. También introduciremos las Promesas, que son una nueva figura de JavaScript especialmente ideada para afrontar acciones asíncronas. Pero empecemos en este tema por definir los conceptos básicos de los generadores.

Figura
Figura. Qué hay dentro de un objeto iterador.

En el siguiente código observamos una muy simple función generadora. La ejecución de un generador nos devuelve siempre un objeto iterador, como se observa en la Figura. En el tema sobre iterables se expone más extensamente para qué sirve un iterador, que básicamente tiene la finalidad de iterar por un objeto.

Todo iterador debe tener al menos un método next(). El del generador tiene además los métodos return() y throw() que explicaremos en apartados posteriores. Veámos ahora el método next(), cuya ejecución nos devolverá un objeto resultado. Está compuesto por una propiedad value con el valor devuelto y otra done que nos indica si ya finalizó la iteración.

function* gen(){
    yield 1;
    yield 2;
}
let iter = gen();
console.log(typeof iter); // object
console.log(iter); // gen {[[GeneratorStatus]]: "suspended", 
                   // [[GeneratorReceiver]]: Window}
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
console.log(iter.next()); // Object {value: undefined, done: true}
    

En el tema de iterables vimos que una función iteradora se caracteriza por devolver un objeto iterador que contiene un método next(). Así que el anterior generador podría haberse construido con la función iteradora del siguiente código. Se observa claramente que la función generadora consigue lo mismo sólo con un par de líneas de código.

function gen(){
    let index = 0;
    return {
        next() {
            index++;
            if (index<3){
                return {value: index, done: false};
            } else {
                index = 0;
                return {value: undefined, done: true};
            }
        }
    };
}
let iter = gen();
console.log(typeof iter); // object
console.log(iter); // Object: {next: function()}
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
console.log(iter.next()); // Object {value: undefined, done: true}
    

Como vimos en el tema de iterables, todo objeto iterador puede ser fuente de datos de un bucle for-of, destructuring, operador de propagación de Array o métodos como Array.from(iterable):

function* gen(){
    yield 1;
    yield 2;
}
//Iterador puede ser fuente de un for-of
for (let item of gen()){
    console.log(item); // 1
                       // 2
}
//O del operador de propagación de Array
console.log([...gen()]); // [1, 2]
//O para destructuring
let [a, b] = gen();
console.log(a, b); // 1 2
//O del método Array.from()
console.log(Array.from(gen())); // [1, 2]
    

Un generador es una función

Un generador no deja de ser una función. No podemos crearla con el operador new, pero se comporta como una función y se ejecuta como una función. Podría ser una expresion de función:

let gen =  function*(){
    yield 1;
    yield 2;
}
let iter= gen();
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2
    

E incluso una función anónima autoejecutable, tras lo cual sólo quedaría el iterador del generador. Siempre iteramos sobre el iterador, no sobre el generador. El código siguiente nos permitirá sólo ejecutar dos next(), pues a partir del tercero ya habrá finalizado y no hay forma de volver a ejecutar el generador pues no quedó referencia alguna.

let iter = (function*(){
    yield 1;
    yield 2;
})();
console.log(iter.next().value); // 1
console.log(iter.next().value); // 2
console.log(iter.next().value); // undefined
    

También podemos pasar argumentos en la ejecución de la función. En el siguiente código devolvemos el único argumento con el primer next().

function* gen(a){
    yield a;
}
let iter = gen(1);
console.log(iter.next()); // Object {value: 1, done: false}
    

Efecto de return en un generador

La expresión yield puede traducirse como producción de un valor frente a return que es una devolución de un valor. Sabemos que toda función en JavaScript siempre devuelve un valor. Y que cuando en una función (generadora o no) se omite return se devolverá el valor undefined. Ambas devoluciones yield y return pueden incluirse en un generador, pero debemos entender qué es lo que pasará cuando el generador encuentre un return.

En el tema sobre iterables vimos como un método con return nos podía servir de cierre de un iterador. Para un generador también sucede lo mismo con un return. En el código siguiente vemos que cuando encuentra return devolverá el valor 2 y finalizará la iteración con done puesto a verdadero. La última producción yield 3 resultará, por tanto, inalcanzable.

function* gen(){
    yield 1;
    return 2;
    yield 3;
}
let iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: true}
//La iteración ya finalizó: no hay nada más que devolver
console.log(iter.next()); // Object {value: undefined, done: true}
console.log(iter.next()); // Object {value: undefined, done: true}
    

Sin embargo, cuando usamos un objeto iterador desde un generador como fuente de datos de otras estructuras, la aparición de return finalizará anticipadamente la iteración. En el código siguiente vemos que se ignora el valor 2 devuelto:

function* gen(){
    yield 1;
    return 2;
    yield 3;
}
//Return finaliza la iteración ignorado
//cualquier valor devuelto
for (let item of gen()){
    console.log(item); // 1
}
console.log([...gen()]); // [1]
let [a, b] = gen();
console.log(a, b); // 1 undefined
console.log(Array.from(gen())); // [1]
    

Todo iterador ha de tener un método next(). Pero también puede tener un método return(), como se observa en la Figura en un apartado anterior. En el siguiente ejemplo obtenemos el primer yield 1 y cerramos el generador al enviarle un return(). Se comporta como si hubiésemos puestos un return después del yield 1. Vea que los siguientes next() no devolverán valores, pues el generador está cerrado.

function* gen(){
    yield 1;
    yield 2;
}
let iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.return()); // Object {value: undefined, done: true}
console.log(iter.next()); // Object {value: undefined, done: true}
    

Doble comportamiento de yield

Supongamos que quiero construir un generador pasándole como argumento un número entero. Cada vez que llame a next() quiero que me devuelva los números enteros correlativos a partir de ese primero. Podría ser algo como el código siguiente, donde enviamos un primer número 3 y esperamos obtener la lista 3, 4, 5, ... con los sucesivos next():

function* gen(a){
    while (true){
        a = 1 + (yield a);
    }
}  
let iter = gen(3);
console.log(iter.next()); // Object {value: 3, done: false}
console.log(iter.next()); // Object {value: NaN, done: false}
console.log(iter.next()); // Object {value: NaN, done: false}
    

Pero lo anterior no funciona. Con el primer next() la ejecución se detiene en el yield devolviendo el argumento correctamente. Pero para las siguientes ejecuciones obtenemos NaN. ¿Porqué?

El asunto está en el doble comportamiento de la expresión yield. Por un lado actúa como una devolución cuando el código se detiene. En ese caso devuelve lo que venga a continuación. Con el primer next() la parada en (yield a) devolverá el primer valor 3 del argumento.

Por otro lado la expresión yield actúa como receptor del argumento de un next(). Cuando reanudemos la ejecución con el siguiente next(), el argumento de este método será sustituido en el lugar de toda la expresión yield. Como el segundo next() equivale a next(undefined), recibimos entonces undefined. Al hacer a = 1 + undefined obtendremos un NaN, que tras pasar a la siguiente iteración del while ocurrirá la pausa en el (yield a) con ese valor NaN.

Entonces no hay que olvidar que desde un next() siempre recibiremos un valor que será asignado al yield detenido en el next() anterior. A excepción del primero que no se asigna nada pues no hay pausa anterior. Cuando no se envía argumento se estará recibiendo implícitamente el valor undefined. Para generar los números correlativos a partir de uno dado de nuestro ejemplo podríamos usar el siguiente código:

function* gen(a){
    while (true){
        a += yield a;
    }
}  
let iter = gen(3);
//Genera números correlativos de uno en uno
console.log(iter.next()); // 3
console.log(iter.next(1)); // 4
console.log(iter.next(1)); // 5
//O de 10 en 10
console.log(iter.next(10)); // 15
console.log(iter.next(10)); // 25
    

Ante todo no hay que olvidar que yield es o puede formar parte de una expresión. A diferencia de return que es una sentencia y no puede formar parte de una expresión. Sería un error hacer (return 1) + (return 2), pero si que podemos hacer (yield 1) + (yield 2). E incluso combinar ambas devoluciones en una sóla sentencia, como el código siguiente:

function* gen(){
    return (yield 1) + (yield 2);
}
let iter = gen();
console.log(iter.next()); // 1
console.log(iter.next("a")); // 2
console.log(iter.next("b")); // "ab"    
    

Al igual que en el código anterior, en ciertas circunstancias hemos de rodear el yield con paréntesis. Si no lo hacemos nos dará un error de sintaxis, como en el código siguiente:

function* gen(a){
    let x = 3 * yield a;
    yield x;
}
let iter = gen(7); //SyntaxError: Unexpected identifier
    

Con los paréntesis indicamos que en la ejecución de next(100) ahí se sustituirá el valor del argumento. Será entonces multiplicado por el número 3 y asignado a la variable x, deteniéndose el generador para producir ese valor 300 en la siguiente expresión yield x.

function* gen(a){
    let x = 3 * (yield a);
    yield x;
}
let iter = gen(7);
console.log(iter.next()); // 7
console.log(iter.next(100)); // 300
    

Cuando el yield está solitario en la parte derecha de una asignación no son necesarios los paréntesis:

function* gen(a){
    let x = yield a;
    yield x * 3;
}
let iter = gen(7);
console.log(iter.next()); // 7
console.log(iter.next(100)); // 300
    

En cualquier caso debemos cuidar la expresión, pues no es lo mismo yield a + 1, que (yield a) + 1. En el primer caso todo lo que sigue hasta el final de la expresión es la expresión del yield. En el segundo caso se delimita claramente hasta donde llega el yield.

//Aclarar hasta dónde llega el yield
function* gen(a){
    yield a + 1;
    (yield a) + 1;
}
let iter = gen(7);
console.log(iter.next()); // 8
console.log(iter.next()); // 7
    

Generador como productor de valores

Un generador puede verse desde la perspectiva de productor de valores. En ese caso la producción yield devuelve un valor mientras que ignoramos lo que recibimos en esa producción. Como ejemplo podemos hacer un generador de números primos. Vemos en el siguiente código que no hacemos nada con lo que recibimos en yield n, sólo envíamos el valor del número primo al exterior.

function* primos(desde, hasta){
    for (let n=desde; n<=hasta; n++) {
        if (n % 2 > 0) {
            let sq = Math.floor(Math.sqrt(n));
            if (sq%2==0) sq--;
            let esPrimo = true;
            for (let i=sq; i>2; i=i-2){
                if (n % i == 0){
                    esPrimo = false;
                    break;
                }
            }
            if (esPrimo) yield n;
        }
    }
}
console.log([...primos(8, 20)]); // [11, 13, 17, 19]
    

Observe otra vez como un generador es también un Iterable. Así puede ser fuente de un bucle for of y de otros casos, entre ellos el operador de propagación de Array como en el ejemplo. Puede ver este ejemplo en ejecución interactiva:

Ejemplo: Generador de números primos

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Puede ver también como realizar este ejemplo con una función iteradora en el temas sobre Iterables.

Generador como consumidor de valores

La otra perspectiva es que el generador actúe como un consumidor de valores. En el siguiente código el generador recibe valores y los inserta en orden en un Array. Usa los nuevos métodos de Array en ES6 findIndex() y copyWithin(). Alguién podría decir que para qué es necesario todo esto teniendo el método sort() que ordena un Array. Podría insertar valores con push() y ordenar el Array con sort(). Pero como siempre, estos son sólo ejemplos de aplicación para acompañar la descripción de un concepto. Lo que hagamos dentro del generador podría ser cualquier otra cosa más complicada que una simple inserción en orden.

function* generadorInsertar(arr){
    while(true){
        let valor = yield;
        let index = arr.findIndex(v => v > valor);
        arr.push(valor);
        if (index>-1){
            //desplaza a la derecha para abrir hueco
            arr.copyWithin(index+1, index);
            arr[index] = valor;
        }
    }
}
//Le pasamos un array vacío para iniciar generador
let unArray = [];
let iter = generadorInsertar(unArray);
iter.next();
//Vamos metiendo valores
iter.next(3);
iter.next(1);
iter.next(6);
iter.next(4);
iter.next(2);
iter.next(5);
console.log(unArray); //[1, 2, 3, 4, 5, 6]
    

Observamos que let valor = yield no acompaña nada tras la producción. De hecho sí que se está devolviendo algo y es undefined. Pero esa devolución se ignora en el exterior pues no la necesitamos para nada.

El ejemplo anterior parece que tiene pocos motivos para usar un generador, pues con una simple función se podría conseguir lo mismo.

function insertar(arr, valor){
    let index = arr.findIndex(v => v > valor);
    arr.push(valor);
    if (index>-1){
        arr.copyWithin(index+1, index);
        arr[index] = valor;
    }
}

let unArray = [];
insertar(unArray, 3);
insertar(unArray, 1);
insertar(unArray, 6);
insertar(unArray, 4);
insertar(unArray, 2);
insertar(unArray, 5);
console.log(unArray); //[1, 2, 3, 4, 5, 6]
    

Entonces ¿qué ventaja aporta usar un generador para este caso? Ambos parecen que tienen el mismo código y por tanto se van a comportar igual. Pero no es así. Supongamos que en este segundo caso insertamos algunos valores, vacíamos el Array y volvemos a insertar. Es esperable que se descarten las primeras inserciones y sólo tengamos las últimas.

let unArray = [];
insertar(unArray, 3);
insertar(unArray, 1);
insertar(unArray, 6);
//Vacíamos el array (reasignamos variable)
unArray = [];
insertar(unArray, 4);
insertar(unArray, 2);
insertar(unArray, 5);
console.log(unArray); // [2, 4, 5]
    

Eso pasa porque cada vez que insertamos un valor estamos realizando una nueva ejecución de la función. Y en cada ejecución le estamos pasando una nueva referencia a un Array externo. Recuerde que para valores que no sean tipos primitivos lo que se pasa en los argumentos es una referencia al objeto origen.

Sin embargo el generador se ejecuta como función una primera vez. En ese momento le pasamos la referencia a un Array externo. Ese generador iniciado es una función en ejecución y por tanto esa referencia sigue viva. Si reasignamos el Array esa referencia se pierde. Dentro del generador seguirá existiendo un Array que no se puede referenciar desde el exterior, así que no hay nada que actualizar fuera del generador. Si en lugar de haber hecho una reasignación unArray = [] lo hubiésemos vaciado con unArray.length = 0 la referencia no se hubiese roto y finalmente se verían las nuevas inserciones [2, 4, 5]. Puede ver más sobre diversas formas de vaciar un Array.

let unArray = [];
let iter = generadorInsertar(unArray);
iter.next();
iter.next(3);
iter.next(1);
iter.next(6);
//Vacíamos el array (reasignamos variable)
unArray = [];
iter.next(4);
iter.next(2);
iter.next(5);
//El Array esta vacío ???
console.log(unArray); // []
    

Este efecto de función en ejecución que tienen los generadores nos puede servir para proteger un Array mientras realizamos operaciones. Supongamos que partimos de un Array vacío y la única operación permitida es insertar valores en orden. En un momento dado podemos finalizar las operaciones obteniendo ese Array. Estos requerimientos podrían conseguirse sin generadores, pero con ellos es más simple y efectivo.

El siguiente código es el mismo que usamos antes, sólo que le agregamos un return para que devuelva el Array interno cuando le enviémos un segundo next() con argumento vacío. Recuerde que el primer next() vacío sólo sirve para posicionar el generador en el primer yield.

function* generadorInsertar(arr){
    while(true){
        let valor = yield;
        if (valor === undefined) return arr;
        let index = arr.findIndex(v => v > valor);
        arr.push(valor);
        if (index>-1){
            arr.copyWithin(index+1, index);
            arr[index] = valor;
        }
    }
}
//Abrimos el generador con un Array vacío.
//No hay referencia aquí a ese Array: lo único
//que podemos hacer es insertar valores
let iter = generadorInsertar([]); iter.next();
//Vamos metiendo valores
iter.next(3);
iter.next(1);
iter.next(6);
//Cerramos el generador devolviendo el Array ordenado
let unArray = iter.next().value;
console.log(unArray); // [1, 3, 6]
//Si queremos meter más valores abrimos de nuevo 
//el generador pasándole unArray ya con valores
//ordenados (aunque este generador no lo verificará)
iter = generadorInsertar(unArray); iter.next();
iter.next(4);
iter.next(2);
iter.next(5);
unArray = iter.next().value;
console.log(unArray); // [1, 2, 3, 4, 5, 6]
    

A continuación puede ver este ejemplo en forma interactiva. La opción de Preservar valores al cerrar guarda el Array obtenido al cerrar y la siguiente vez que se inicie el generador lo usará como Array de partida. En otro caso se iniciará con uno vacío.

Ejemplo: Generador como consumidor de valores

Valores consumidos:
Array generado: []
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Paso de mensajes

Combinando ambas perspectivas de un generador como productor y consumidor de valores, podríamos entender que se produce una comunicación mediante un paso de mensajes entre un generador y el exterior. La página envía su mensaje en el argumento de un next() y el generador le responderá con la devolución del siguiente yield. Veámos si eso puede configurarse así con el siguiente ejemplo.

Para buscarle una aplicación práctica, supongamos que tenemos que construir un generador que haga de calculadora operando un número contra un acumulado. En el código siguiente del generador se observa la lína question = yield response, una antes del bucle y otra al final del mismo. Ahí se produce la pausa del generador enviando la devolución del yield response y quedando a la espera del siguiente mensaje que será asignado a la variable question.

function* calculador(){
    let acumulado = 0, operadores = ["+", "-", "*", "/"];
    let response = "calculador iniciado";
    let question = yield response;
    while (true){
        if (question == "fin"){
            return "Calculador finalizado";
        } else {
            let operador = question[0];
            if (operadores.includes(operador)){
                let operando = parseFloat(question.substr(1));
                acumulado = (operador=="+") ? acumulado+operando :
                            (operador=="-") ? acumulado-operando :
                            (operador=="*") ? acumulado*operando :
                            acumulado/operando;
                response = acumulado;
            } else {
                response = "Error en operador ";
            }
        }
        question = yield response;
    }
}
    

Iniciaríamos una conversación ejecutando el generador y obteniendo un objeto iterador sobre el que enviaríamos operaciones en el argumento de next() y el generador le respondería con el yield. La comunicación finaliza cuando se envía la palabra fin, en cuyo momento el generador realiza la devolución con un return. Al estar cerrado el generador, los siguientes next() resultarán undefined.

function iniciarCalculador(){
    var iter = calculador();
    iter.operar = function (valor){
        return iter.next(valor).value;
    };
    console.log(iter.next().value);
    return iter;
}
//Iniciamos y envíamos y recibimos mensajes
let com = iniciarCalculador(); // Calculador iniciado
console.log(com.operar("+2")); // 2
console.log(com.operar("+3")); // 5
console.log(com.operar("/4")); // 1.25
console.log(com.operar(123)); // Error en operador 
console.log(com.operar("fin")); // Calculador finalizado
console.log(com.operar("-8")); // undefined
    

Basado en la estructura anterior, el siguiente es un generador con el que podemos conversar enviando y recibiendo mensajes. El código lo puedes consultar en el enlace al pie del ejemplo. Lo importante del ejemplo es entender el concepto de paso de mensajes y como el generador se detiene a la espera de una cuestión que le enviamos desde el exterior. Una vez recibida una cuestión el generador le responde (si sabe) con otro mensaje.

Ejemplo: Paso de mensajes con un generador

[COM] Hola! Soy COM. Díme tu nombre para comunicarnos.
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Control de errores en un generador

Una característica interesante de los generadores es que podemos capturar errores y traerlos al script principal como un mensaje más. Usando la estructura de paso de mensajes del apartador anterior, recibimos mensajes que se esperan en String y los devolvemos tras convertirlos a mayúsculas. Cuando envíamos un número, el generador captura el error y lo devuelve como un mensaje.

function* gen(){
    let response = "Iniciado";
    let question = yield response;
    while (true){
        if (question == "fin") return "Finalizado";
        try {
            response = "Recibido " + question.toUpperCase();
        } catch(e){
            response = "ERROR: " + e.message; 
        }
        question = yield response;
    }
}
let com = gen();
console.log(com.next().value); // Iniciado
console.log(com.next("abc").value); // Recibido ABC
//Envíamos un número que causará error en el generador al tratar 
//de convertirlo a mayúsculas, recibiendo aquí ese mensaje de error
console.log(com.next(123).value); // ERROR: question.toUpperCase is
                                  // not a function
    

Ya vimos que el iterador tiene los métodos next() y return(). Y también tiene un método throw() para lanzar un error dentro del generador. En el siguiente código obtenemos el primer yield 1 y lanzamos un Error simulado dentro del generador. Lo capturamos con un bloque try-catch para que nos devuelva el texto del mensaje. Al ejecutar el return el generado quedará cerrado y los siguientes next() ya no acceden.

function* gen(){
    try {
        yield 1;
        yield 2; 
    } catch(e){
        return e.message;
    }
}
let iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.throw(Error("Error simulado"))); 
            // Object {value: "Error simulado", done: true}
console.log(iter.next()); // Object {value: undefined, done: true}
    

Generadores delegados y recursivos

Un generador puede delegar en otro generador para producir valores. Esto se indica con la expresión yield* otroGen(), siendo otroGen el generador delegado. Lo que se está haciendo es ejecutar ese segundo generador y, por tanto, obteniendo un iterador que producirá su serie de valores para el generador principal.

function* gen(){
    yield 1;
    yield* otroGen();
    yield 2;
}
function* otroGen(){
    yield "a";
    yield "b";
}
console.log([...gen()]); // [1, "a", "b", 2]
    

Se puede delegar en cualquier iterable, como un Array:

function* gen(){
    yield 1;
    yield* ["a", "b"];
    yield 2;
}
console.log([...gen()]); // [1, "a", "b", 2]
    

O los propios argumentos del generador:

function* gen(){
    yield 1;
    yield* arguments;
    yield 2;
}
console.log([...gen("a", "b")]); // [1, "a", "b", 2]    
    

Cuando se delega en el mismo generador podríamos considerarlo como un generador recursivo. En el siguiente ejemplo lo usamos para aplanar un Array. Se trata de convertir Arrays anidados en un Array plano de una dimensión:

function* aplanar(arr){
    let i = -1;
    while (i++, i < arr.length){
        if (Array.isArray(arr[i])){
            yield* aplanar(arr[i]);
        } else {
            yield arr[i];
        }
    }
}
let arrDeep = [1, [2, 3], [[4, 5], 6, [7, 8]], [9, 10]];
console.log([...aplanar(arrDeep)]);
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    

Los anteriores ejemplos sólo producen valores. En una situación donde además haya consumo de valores, un generador delegado devolverá el valor indicado en su return.

function* gen(){
    let x = 1 + (yield "A"); // (A)
    let y = 1 + (yield* genDel()); // (C)
    return [x, y];
}
function* genDel(){
    let z = 1 + (yield "B"); // (B)
    return z;
}
let iter = gen(); 
console.log(iter.next().value);   // A
console.log(iter.next(2).value);  // B
console.log(iter.next(3).value);  // [3, 5]
    

Intentemos seguir los pasos de ese código:

  1. Con el primer next() el generador principal gen() producirá el primer yield "A" y se quedará ahí esperando por un siguiente next(). Recordemos que el valor enviado en el primer next() es ignorado. El primer next() sólo sirve para posicionar el generador en su primer yield.
  2. Con el siguiente iter.next(2) reanudamos gen(), enviando el valor 2 que se sustituye en el yield "A". Entonces calcula el valor de la variable x = 1 + 2 = 3. Continua en la siguiente sentencia donde hay un delegado, por lo que pasará a ese otro generador y se detendrá en su primer yield "B". Produce entonces ese valor "B" y espera ahí por el siguiente next().
  3. Con el último iter.next(3) reanudamos genDel() enviando ese valor 3. La variable pasa entonces a valer z = 1 + 3 = 4. Salimos de este generador delegado devolviendo en el return ese valor de z=4, volviendo al generador principal resolviendo y = 1 + 4 = 5. Finalmente devolvemos el Array [x, y] = [3, 5].

Generadores simultáneos

Podríamos tener dos generadores que produzcan simultánemente sucesiones de números naturales. Más arriba vimos un generador de números primos que vamos a reproducir ahora pero con un bucle infinito. También vamos a usar otro generador para producir números de Fibonacci. El código instancia ambos generadores y en un bucle generamos los diez primeros términos de cada sucesión:

function* primos(){
    let n = 0;
    while (true) {
        if (n<3){
            yield n;
        } else if (n % 2 > 0) {
            let sq = Math.floor(Math.sqrt(n));
            if (sq%2==0) sq--;
            let esPrimo = true;
            for (let i=sq; i>2; i=i-2){
                if (n % i == 0){
                    esPrimo = false;
                    break;
                }
            }
            if (esPrimo) yield n;
        }
        n++;
    }
}
function* fibonacci() {
    let n = -1, ultimo = 1, penultimo = 1;
    while (true) {
        let actual = (n > 1) ? ultimo + penultimo : 1;
        [penultimo, ultimo] = [ultimo, actual];
        yield actual;
        n++;
    }
}
let iterPrimos = primos(); iterPrimos.next();
let iterFibonacci = fibonacci(); iterFibonacci.next();
let listaPrimos = [], listaFibonacci = [];
for (let i=0; i<10; i++){
    listaPrimos.push(iterPrimos.next().value);
    listaFibonacci.push(iterFibonacci.next().value); 
}
console.log("10 Primos: ", listaPrimos); 
//10 Primos: [1, 2, 3, 5, 7, 11, 13, 17, 19, 23]
console.log("10 Fibonacci ", listaFibonacci); 
//10 Fibonacci: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    

Lo anterior es la ejecución de dos generadores independientes. En el bucle extraemos la producción de ambos generadores en un tramo de diez iteraciones. Podríamos volver a ejecutar otro tramos de iteraciones, pues los generadores están suspendidos a la espera de que los reanuden otra vez.

Aprovechando la capacidad de un generador para quedar suspendido preservando el estado de sus variables, sería interesante automatizar múltiples tareas de producción, cada una encomendada a un generador. Podríamos producir los diez primeros términos de ambas sucesiones, volcarlos en la pantalla quedando los dos generadores en estado suspendido. En cualquier momento posterior podríamos volver a producir otro tramo y volcarlos.

Empezaremos viendo una función para iniciar un Array de sucesiones. Cada posición del array es un objeto con el iterador del generador y una referencia a un elemento de la página donde volcaremos los resultados.

let arrSucesiones = [];
function iniciarSucesiones() {
    let iterPrimos = primos(); iterPrimos.next();
    let iterFibonacci = fibonacci(); iterFibonacci.next();
    return [
        {iter: iterPrimos,
        elem: document.getElementById("lista-primos-gen2")
        },
        {iter: iterFibonacci, 
        elem: document.getElementById("lista-fibonacci-gen2")
        }
    ];
}
    

El siguiente paso es construir una función para cada ejecución de tramo. Iteramos por el Array de sucesiones y ejecutamos un tramo mediante el método Array.from(). Pasándole un objeto con longitud igual al tramo que queremos producir, el método from() lo considera un Array-like, creando un Array iterando por esa longitud de valores extraidos con el next() del generador. Finalmente agregamos esa lista parcial a la ya existente en el elemento de la página.

function ejecutarTramo(arrSucesiones, tramo){
    for (let sucesion of arrSucesiones){
        let lista = Array.from({length: tramo}, () => 
            sucesion.iter.next().value);
        if (sucesion.elem.textContent.trim() !== "") 
            sucesion.elem.textContent += ", ";
        sucesion.elem.textContent += lista.join(", ");
    }
}
    

Ejemplo: Dos generadores de sucesiones naturales

Sucesión de primos:
Sucesión de Fibonacci:
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

El ejemplo anterior podría haberse implementado sin generadores. Pero entonces las funciones deberían almacenar el último número de la iteración, pues a diferencia de cuando un generador se encuentra pausado, las funciones cuando finalizan pierden las variables. A menos que usemos variables globales, o mejor, hacer uso del efecto Closure. En el siguiente código la función exterior devuelve una interior, quedando la variable n en el Closure y, por tanto, manteniendo el valor de la última ejecución de la función interior buscar(tramo).

function primos(){
    let n = 0;
    return function buscar(tramo){
        let arr = [];
        while (arr.length<tramo) {
            n++;
            if (n==2){
                arr.push(n);
            } else if (n % 2 > 0) {
                let sq = Math.floor(Math.sqrt(n));
                if (sq%2==0) sq--;
                let esPrimo = true;
                for (let i=sq; i>2; i=i-2){
                    if (n % i == 0){
                        esPrimo = false;
                        break;
                    }
                }
                if (esPrimo) arr.push(n);
            }
        }
        return arr;
    }
}
let buscarTramoPrimos = primos();
console.log(buscarTramoPrimos(10));
// [1, 2, 3, 5, 7, 11, 13, 17, 19, 23]
console.log(buscarTramoPrimos(10));
// [29, 31, 37, 41, 43, 47, 53, 59, 61, 67]
    

El comportamiento es el mismo que el del ejemplo con generadores para la búsqueda de primos, pero usando otro concepto de JavaScript. La ventaja del ejemplo con generadores es que sólo ejecutamos las funciones la primera vez. Luego usando next() recuperamos los tramos siguientes. En el ejemplo del Closure estamos ejecutando la función interna para cada tramo. Y ejecutar una función tiene un coste y otras posibles repercusiones. No es siginificativo con esos ejemplos, pero si podría serlo en otros contextos.

Generadores entrelazados

El ejercicio del apartado anterior ejecutaba un paso de iteración en todos los generadores, de tal forma que se daba una ejecución simultánea de los generadores, avanzando todos al mismo tiempo. Otra situación es que la condición de avanzar en los generadores sea impuesta por ellos mismos. Se podrían llamar generadores entrelazados porque los datos que generan son los que dirigen la ejecución de los generadores.

La primera duda que se plantea es si desde un generador podemos hacer avanzar otro generador. Supongamos primero un ejemplo sin generadores. Tenemos una función para ordenar creciente un Array que va comprobando si un elemento es menor que el anterior, en cuyo caso llama a otra función para insertar en su lugar ese elemento menor en la parte izquierda ya ordenada del Array. El ejemplo ni es muy útil ni muy eficiente, pero nos servirá para exponer este apartado:

function ordenar(arr){
    let i = 0;
    while(i < arr.length-1){
        if (arr[i+1] < arr[i]){
            insertar(arr, i+1);
        }
        i++;
    }
    return arr;
}
function insertar(arr, i){
    while((i--) > -1 && arr[i+1] < arr[i]){
        [arr[i], arr[i+1]] = [arr[i+1], arr[i]];
    }
}
console.log(ordenar([8, 4, 7, 3])); // [3, 4, 7, 8]
    

¿Qué sucederá si convertimos en generadores ambas funciones? Para que la función ordenar() llame a insertar() usando su next() utilizamos la función nextGen(iter, valor). Sin embargo para que funcione es necesario encolar esta tarea.

function nextGen(iter, valor){
    //Con sólo esto no funcionará, necesitamos encolar la tarea
    //iter.next(valor);
    window.setTimeout(() => iter.next(valor));
}
function* ordenar(arr){
    let i = 0;
    let iterInsertar = yield;
    while(i < arr.length-1){
        if (arr[i+1] < arr[i]){
            yield nextGen(iterInsertar, i+1);
        }
        i++;
    }
    nextGen(iterInsertar, -1);
    console.log(arr); // [3, 4, 7, 8]
}
function* insertar(arr){
    let iterOrdenar = yield;
    while (true){
        let i = yield;
        if (i==-1) return;
        while((i--) > -1 && arr[i+1] < arr[i]){
            [arr[i], arr[i+1]] = [arr[i+1], arr[i]];
        }
        iterOrdenar.next();
    }
}

let arr = [8, 4, 7, 3];
let iterOrdenar = ordenar(arr); iterOrdenar.next();
let iterInsertar = insertar(arr); iterInsertar.next();
//Con los segundos next() envíamos el iterador del otro generador
//con el que se entrelaza. Empezamos por insertar() primero, pues
//el generador ordenar() es el que dirigirá el ordenamiento
iterInsertar.next(iterOrdenar);
iterOrdenar.next(iterInsertar);
    

El motivo es que si no encolamos, el iterador de ordenar() está corriendo cuando hagamos yield nextGen(iterInsertar, i+1). En ese momento no producirá error, pero luego en insertar() cuando volvamos con iterOrdenar.next() nos dará un error de que el generador ordenar() está ya corriendo. No podemos hacer next() sobre un generador que no esté pausado. Al encolar, tras la ejecución de setTimeout(), se producirá la pausa en el yield de ordenar(). Luego cuando se ejecute la tarea en la cola llevará a cabo el next() sobre insertar().

Este apartado no deja de ser un simple curiosidad, pues por ahora no le veo mayor utilidad al uso de generadores en esta forma. Pero esto nos ayuda a comprender mejor como funcionan los generadores. El siguiente ejemplo reproduce el código usando un Array con números aleatorios en cada ejecución.

Ejemplo: Generadores entrelazados

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.