Ir al contenido principal

Las bases de generadores en ES6 (JavaScript)

 Traducido de https://davidwalsh.name/es6-generators


Una de las más emocionantes nuevas features que vienen en JavaScript ES6, son una nueva generación de funciones, llamadas generadores. Su nombre es un poco extraño, pero su comportamiento puede parecer mucho más extraño al primer vistazo. Este artículo tiene por objetivo explicar las bases de cómo trabaja, y ayudarlo a entender porqué es tan poderoso para el futuro de Js.


Run-To-Completion

Lo primero que debemos observar al hablar sobre generadores es como difieren de una función normal con respecto a la expectativa de ‘run to completion’.


Ya sea que te hayas dado cuenta o no, tu siempre has podido asumir algo bastante fundamental acerca de tus funciones: siempre que una función haya comenzado a ejecutarse, está seguirá hasta completarse sin que ningún otro código js pueda correr antes.


Por ejemplo:


setTimeout(function(){

    console.log("Hello World");

},1);


function foo() {

    // NOTE: don't ever do crazy long-running loops like this

    for (var i=0; i<=1E10; i++) {

        console.log(i);

    }

}


foo();

// 0..1E10

// "Hello World"

Aquí, al bucle for le tomará bastante tiempo completarse. Beno, en realidad poco más de un milisegundo. Pero nuestro timer callback de la instrucción console.log no puede interrumpir a la función foo() mientras se ejecuta, por lo que se atasca al final de la línea (del event-loop) y espera pacientemente su turno.


¿Sin embargo, qué pasaría si foo() pudiera ser interrumpida? ¿Esto no causaría estragos en nuestros programas?


Son exactamente esos los desafíos de la programación multihilo. Pero somos bastante afortunados en la tierra de JavaScript, no tenemos miedo a tales cosas porque javascript es siempre monohilo (solo un comando/función ejecutándose por vez) 


Nota: Los web workers son un mecanismo por el cual vos podés independizar un hilo para que corra una parte del programa en JavaScript totalmente separado del hilo principal del programa.

La razón por la cual esto no introduce complicaciones multihilo en nuestros programas es que ambos threads sólo pueden comunicarse a través de eventos asíncronos normales. los cuales siempre cumplen el comportamiento del bucle de eventos ‘uno a la vez’ requerido para  el run-to-completion.


Run-Stop-Run


Con los generadores de ES6, tenemos un nuevo tipo de función, que puede ser pausada en el medio, una o varias veces, y retomada luego; permitiendo al resto del código ejecutarse durante los periodos de pausa.


Si alguna vez has leído algo sobre concurrencia o programación de hilos, puede que hayas visto el término ‘cooperativo’, el cual básicamente indica que un proceso (o en nuestro caso, una función) elige cuándo permitirá una interrupción, para que pueda cooperar con otro código. Este concepto contrasta con ‘preventivo’, que sugiere que una función/proceso puede ser interrumpida contra su voluntad.


Los generadores de ES6 son ‘cooperativos’ en su comportamiento de concurrencia. Dentro del cuerpo de la función generador, usarás la palabra clave yield para pausar el proceso dentro de sí misma. Nada puede pausar a un generador desde afuera, se detiene solo cuando se encuentra con un yield.


Como sea, una vez un generador se ha pausado a sí mismo, no puede reanudarse solo. Un control externo debe ser usado para reiniciar el generador. Les explicaré cómo en un momento.


Entonces, básicamente, una función generadora puede detenerse y retomarse tantas veces como vos elijas. De hecho, podés definir una función generador con un loop infinito (como el famoso while(true)) que esencialmente nunca termine. Si bien esto suele ser una locura o un error en un programa Js,. con las funciones generadoras es perfectamente sensato y, en ocasiones, es exactamente lo que quieres.


Aún más importante, esta detención e inicio no es solo un control sobre la ejecución del generador, sino que también permite que el mensaje two-way entre y salga del generador a medida que avanza. Con las funciones normales tenés los parámetros al principio y el retorno al final. Con funciones generador, mandás un mensaje con cada yield, y otro de vuelta cada vez que lo reinicies.


¡Sintaxis por favor!


Profundicemos en la sintaxis de estas nuevas y emocionantes funciones generadoras.

Primero, la nueva declaración de sintaxis.


function *foo() {

    // ..

}

Notás el + acá? Esto es nuevo y se ve un poco raro. Para los de otros lenguajes, esto puede parecer un horrible return-value pointer. ¡Pero no te confundas! Esto es solo una forma de señalar que esta función será de tipo generador. 


Probablemente hayas visto otros artículos/documentación que usan function* foo(){ }en lugar de function *foo(){ } (la diferencia es el lugar del *). Ambas son válidas, pero recientemente he decidido que la segunda forma es más precisa, así que seo es lo que estoy usando aquí.


Ahora, voy a hablarles sobre el contenido de los generadores. Las funciones de generador son como las funciones normales en la mayoría de los aspectos. Hay muy poca sintaxis nueva dentro de las funciones de generador.


El principal nuevo juguete que tenemos para jugar, como mencioné más arriba, es la palabra reservada yield. yield__ es una expresión (no una declaración ) porque cuando reiniciamos el generador, le enviaremos un valor de vuelta, y lo que enviamos será un resultado calculado de la expresión yield__ .


Por ejemplo:


function *foo() {

    var x = 1 + (yield "foo");

    console.log(x);

}

La expresión yield “foo” enviará el string “foo” al pausar el generador en ese punto, y cada vez vez que se reinicie el generador. El resultado en la consola será el de la expresión más uno.


¿Ves la comunicación bidireccional? Envías el valor "foo", te detienes, y en algún momento más tarde (¡podría ser inmediato, podría tardar mucho!), El generador se reiniciará y te devolverá un valor. Es casi como si la palabra clave yield fuera una especie de solicitud de un valor.


Podés usar el yield en cualquier lugar de la expresión/declaración, allí yield tomará el valor de indefinido a menos que se le envíe un parámetro. Entonces:


// note: `foo(..)` here is NOT a generator!!

function foo(x) {

    console.log("x: " + x);

}


function *bar() {

    yield; // just pause

    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`

}

Generador iterador

“Generador iterador” Todo un tema ¿no?


Los iteradores son un tipo especial de comportamiento, un patrón de diseño en realidad, donde avanzamos por un conjunto de valores ordenados uno a la vez llamando a next(). Imagina que usas un iterador en un array que tiene cinco valores en el: [1,2,3,4,5]. El primer next() retornaría 1, el segundo next() retornaría 2, y así. Luego de que todos los valores hayan sido retornados, next() retornaría null o false o te indicará que ya ha iterado sobre todos los valores del contenedor de datos. 


La forma en la que controlamos al generador desde afuera es construyendo e interactuando con un generador iterador. Esto suena mucho más complicado de lo que realmente es. Considerá el siguiente ejemplo: 


function *foo() {

    yield 1;

    yield 2;

    yield 3;

    yield 4;

    yield 5;

}

Para recorrer los valores de esa función generador * foo (), necesitamos un iterador para ser construido. ¿Cómo hacemos eso? ¡Fácil!


var it = foo();

Oh! Entonces, llamar a la función del generador de la manera normal en realidad no ejecuta ninguno de sus contenidos.


Es un poco raro de entender. También podés sentirte tentado a preguntarte, ¿por qué no es var it = new foo()? Los por qué detrás de la sintaxis son complicados y están más allá de nuestro alcance de discusión aquí.


Ahora, para comenzar a iterar en nuestra función de generador, simplemente hacemos lo siguiente:


var message = it.next();

Esto devolverá nuestro 1 de la declaración yield 1 , que tiene la propiedad value  con el valor devuelto en el yield, y done es un booleano que indica si la iteración terminó o no.


Sigamos con nuestra iteración:


console.log( it.next() ); // { value:2, done:false }

console.log( it.next() ); // { value:3, done:false }

console.log( it.next() ); // { value:4, done:false }

console.log( it.next() ); // { value:5, done:false }

Es interesante notar que done aún es falso cuando obtenemos el valor de 5. Eso es porque técnicamente, la función del generador no está completa. Todavía tenemos que llamar a una llamada next () final, y si enviamos un valor, debe establecerse como resultado de esa expresión de yield 5. Solo entonces se completa la función del generador.


Entonces, ahora:


console.log( it.next() ); // { value:undefined, done:true }

El resultado final de nuestra función generador fue que completamos la función, pero no se dio ningún resultado (ya que ya habíamos agotado todas las declaraciones de yield__).


Puede que te preguntes en este punto, ¿puedo usar el retorno de una función generadora y, si lo hago, ese valor se envía en la propiedad value?


Sí…


function *foo() {

    yield 1;

    return 2;

}


var it = foo();


console.log( it.next() ); // { value:1, done:false }

console.log( it.next() ); // { value:2, done:true }

Y no…


Puede que no sea una buena idea confiar en el valor de retorno de los generadores, porque al iterar las funciones del generador con loops for..of (ver más abajo), el return final sería desechado.


Para completar, también echemos un vistazo al envío de mensajes dentro y fuera de una función de generador a medida que lo iteramos:


function *foo(x) {

    var y = 2 * (yield (x + 1));

    var z = yield (y / 3);

    return (x + y + z);

}


var it = foo( 5 );


// note: not sending anything into `next()` here

console.log( it.next() );       // { value:6, done:false }

console.log( it.next( 12 ) );   // { value:8, done:false }

console.log( it.next( 13 ) );   // { value:42, done:true }


Podés notar que todavía podemos pasarle parámetros (x en nuestro ejemplo) con la primera instancia del iterador foo( 5 ), al igual que en las funciones normales, haciendo que x sea igual a 5


En la primera llamada next(...), no enviamos nada. ¿Por qué? Porque no hay expresión de yield para recibir lo que pasamos.


Pero si pasaramos un valor a la primera llamada next(...), no pasaría nada malo. Solo sería un valor descartado. ES6 dice que las funciones generador ignoran el valor no utilizado en este caso. (Nota: al momento de escribir, los nightlies de Chrome y FF están bien, pero es posible que otros navegadores aún no sean totalmente compatibles y puedan arrojar un error incorrectamente en este caso).


El yield (x + 1) es lo que envía el valor 6. La segunda llamada next(12) envía 12 a esa expresión de yield (x + 1) en espera, por lo que y se establece en 12 * 2, el valor 24. Luego, el yield (y / 3) (yield (24 / 3)) es lo que envía el valor 8. La tercera llamada next(13) envía 13 a esa expresión de yield (y / 3) en espera, haciendo que z se establezca en 13.


Finalmente, return (x + y + z) es return (5 + 24 + 13), o 42 se devuelve como último value.


Volvé a leer eso algunas veces. Es extraño para la mayoría, las primeras veces que lo ven.


for..of

ES6 también adopta este patrón de iterador en el nivel sintáctico, proporcionando soporte directo para ejecutar iteradores hasta su finalización: el bucle for..of.


Ejemplo:


function *foo() {

    yield 1;

    yield 2;

    yield 3;

    yield 4;

    yield 5;

    return 6;

}


for (var v of foo()) {

    console.log( v );

}

// 1 2 3 4 5


console.log( v ); // still `5`, not `6` :(

Como puede ver, el iterador creado por foo() es capturado automáticamente por el bucle for..of, y se itera automáticamente, una iteración para cada valor, hasta que de done:true. Siempre que done sea false, automáticamente extrae la propiedad value y la asigna a su variable de iteración (v en nuestro caso). Una vez done  es true, la iteración del ciclo se detiene (y no hace nada con ningún value final devuelto, si lo hay).


Como señalé anteriormente, podés ver que el bucle for..of ignora y tira el valor de return 6. Además, como no hay una llamada next() expuesta, el bucle for..of no se puede usar en situaciones en las que necesita pasar valores a los pasos del generador como lo hicimos anteriormente.


Resumen

Bien, eso es todo por lo básico de los generadores. No te preocupes si todavía es un poco alucinante. ¡Todos nos hemos sentido así al principio!


Es natural preguntarse qué hará, en la práctica, este nuevo juguete exótico. Sin embargo, hay mucho má. Acabamos de arañar la superficie. Así que tenemos que sumergirnos más profundamente antes de poder descubrir cuán poderosos pueden ser / serán.

Después de haber jugado con los fragmentos de código anteriores, pueden surgir las siguientes preguntas:


¿Cómo funciona el manejo de errores?

¿Puede un generador llamar a otro generador?

¿Cómo funciona la código asíncrono con generadores?

Esas preguntas, y más, se tratarán en artículos posteriores aquí, ¡así que quedáte atento!


Comentarios