JavaScript 2015 (VI). Generadores.

Facebooktwittergoogle_pluslinkedinmailFacebooktwittergoogle_pluslinkedinmail

Cuando estamos recorriendo un bucle nos encontramos, en ocasiones, con que, dada una condición o evento determinado, es necesario interrumpir la ejecución de dicho bucle, retornando el control al proceso JavaScript que lo invocó. Al mismo tiempo, es necesario conservar el valor de una variable (o de más de una) de tal modo que, cuando vuelva a invocarse el bucle, no se inicialice de nuevo, sino que conserve el valor que tenía.

Esta es una necesidad que se nos ha dado a todos alguna vez y, dado que, hasta ahora, no era posible hacer esto, hemos obviado dicha necesidad, buscando soluciones alternativas, en ocasiones muy enrevesadas, para lograr el resultado deseado.

Por primera vez, JavaScript nos ofrece la posibilidad de ejecutar un bucle, interrumpiendolo cuando sea necesario, y manteniendo los valores adquiridos en el mismo por una o más variables. Estas herramientas se conocen con el nombre de Generadores.

ESTRUCTURA DE UN GENERADOR

Vamos a empezar definiendo la forma básica de crear y emplear un generador. Este primer ejemplo no tendrá, realmente, ninguna utilidad práctica, pero sí contará con un inestimable valor didáctico.

Un generador se define siempre como una función, cuyo nombre va precedido de un asterisco. El asterisco es el que le indica al intérprete de JS6 que la función no es, realmente un función de usuario convencional, sino un generador. La sintaxis general obedece al siguiente esquema:

function *generador(){
    ///// Cuerpo del generador
}

Veamos que hay dentro del cuerpo del generador. Realmente el núcleo de un generador es, por su propia naturaleza, un bucle, que puede ser finito o infinito. En este caso lo vamos a crear con un bucle infinito, para el primer ejemplo. Luego entraremos en más detalles.

Dentro del bucle, se pueden manipular variables que se pueden extraer del bucle, deteniendo la ejecución del mismo, mediante la instrucción yield, nativa de JS6. Para entender un poco mejor esto, observa el siguiente código:

Veamos lo que ocurre. En primer lugar, declaramos el generador, con el nombre generador (sí; una vez más, la imaginación al poder). Dentro empezamos declarando una variable con un valor inicial de 0. La hemos llamado contador. A continucación encontramos un bucle infinito. En teoría, al ser infinito, una vez iniciada su ejecución ya no debería detenerse hasta que se forzara dicha detención con la clásica sentencia break. Sin embargo, en este caso empleamos una alternativa a break, llamada yield. Esta instrucción (algunos la llaman operador; aquí no vamos a entrar en polémicas sobre eso) realiza varias funciones:

  • Interrumpe la ejecución del bucle infinito.
  • Retorna el valor que tiene, en ese momento, la variable especificada.
  • Memoriza ese valor, de forma que, en la próxima invocación, lo mantendrá “recordado”.
  • Memoriza el punto exacto donde se produce la interrupción, de forma que, en la próxima invocación, continuará ejecutándose el bucle, no desde el principio del mismo, sino desde la instrucción siguiente a yield.

La declaración completa del generador, por tanto, queda así:

function *generador(){
    var contador = 0;
    while (true){
        yield contador;
        contador ++;
    }
}

Este código corresponde con las líneas que ves, con comentarios aclaratorios, entre la 12 y la 24 del listado anterior.

El siguiente paso es crear un objeto que nos permita usar el generador. Al ser, en definitiva, una función, sólo se carga en memoria, pero no hace nada hasta que es invocada de algún modo. Para invocar este tipo de funciones es necesario, como paso previo, crear un objeto que las referencie. Lo hacemos así:

var usarGenerador = generador();

A pesar de haber creado el objeto, este, por sí mismo, sólo referencia la función, pero no la ejecuta, ni hace mada más. Para ejecutar nuestro generador, el objeto que hemos creado cuenta con el método next(), como vemos a continuación:

usarGenerador.next()

Fíjate que en el listado hay cuatro de estas instrucciones, incluidas dentro de console.log() para volcar el resultado en la consola del navegador. Cuando lo ejecutas (con la consola abierta, claro), lo que ves es lo siguiente:

Object {value: 0, done: false}
Object {value: 1, done: false}
Object {value: 2, done: false}
Object {value: 3, done: false}

Cuando se ejecuta la primera vez el método next() (en la línea 36 del listado), se inicia el generador. Es decir, se incializa la variable contador con el valor 0 (línea 13 del listado) y se entra en el bucle while que empieza en la línea 14 del listado. Se empieza a ejecutar el cuerpo del bucle y llegamos a la línea 17 del listado. La instrucción yield hace lo que se espera de ella. Devuelve el valor de contador e interrumpe la ejecución del listado. Como está especificado, el valor de contador (que, recordemos, se había inicializado a 0) es mostrado en consola:

Object {value: 0, done: false}

El valor de la variable contador aparece con la clave value. La clave done con el valor false que aparece vamos a ignorarla de momento. Hablaremos de ella más adelante.

Ahora volvemos a encontrar una invocación al método next() (línea 37 del listado). Lo que hace el generador en este punto es continuar la ejecución del bucle desde la línea siguiente a donde se interrumpió la última vez (en este listado, continúa desde la línea 20). Esta incrementa el valor de contador. Como hasta ahora valía 0, ahora vale 1. La ejecución continúa y, cómo ya no hay más instrucciones en el bucle, se inicia la siguiente iteración del mismo. Con esto se llega de nuevo a la línea 17, en la que yield hace lo mismo que antes: retorna el valor de contador (que, recuerda, ahora es 1) e interrumpe la ejecución del bucle. El resultado en consola es el siguiente:

Object {value: 1, done: false}

Las líneas 38 y 39 repiten el proceso y, cómo ves, en cada caso se muestra el valor que contador tiene en ese momento.

MÁS INTERRUPCIONES

Dentro del bucle que constituye el generador, podemos incluir tantas sentencias yield como consideremos necesarias si, por ejemplo, necesitamos que el bucle se ejecute en más pasos. Observa el siguiente listado:

El resultado en consola, en esta ocasión, es el siguiente:

Object {value: 0, done: false}
Object {value: 0, done: false}
Object {value: 1, done: false}
Object {value: 0.5, done: false}
Object {value: 2, done: false}
Object {value: 1, done: false}
Object {value: 3, done: false}
Object {value: 1.5, done: false}
Object {value: 4, done: false}
Object {value: 2, done: false}

Cuando se ejecuta el generador la primera vez (en la línea 39), el bucle se ejecuta, en primera iteración, hasta la línea 17, devolviendo el valor de contador que, en ese momento, es 0.

Al ejecutar la segunda vez (en la línea 40), el bucle continúa ejecutándose (recordemos, en su primera iteración) desde la línea 18. Al llegar a la línea 20, la segunda intrucción yield retorna el valor de contador dividido por 2 (0/2 = 0), e interrumpe la ejecución.

Llegamos a la línea 41 y se continúa ejecutando el bucle desde la línea 21. Al llegar a la 23, se incrementa en una unidad el valor de contador, con lo que pasa a valer 1. El bucle continúa ejecutándose, llega a su final físico en la línea 26 e inicia la siguiente iteración. Al llegar a la línea 17 se detiene la ejecución, devolviendo el valor de contador en ese momento (1).

Seguimos ejecutando en la línea 42. Se continúa la ejecución del bucle (recuerda que estamos en la segunda iteración, con el valor de contador a 1) y llega hasta la línea 20. En ese punto, la segunda instrucción yield devuelve 0.5 (1/2 = 0.5). En este momento, se interrumpe la ejecución del bucle.

El proceso se repite para cada vez que se invoca el método next(). Como ves, cada vez se ejecuta el bucle hasta que encuentra una instrucción yield y, en la siguiente llamada, continúa ejecutando desde la línea inmediatamente posterior al último yield que interrumpió la ejecución.

EL GENERADOR ES MULTIUSOS

Del mismo modo que una función de usuario puede ser, una vez definida, invocada por diferentes procesos, un generador puede ser usado por diferentes objetos de generador, y para cada uno guardará una memoria específica de los valores de las variables que se modifiquen durante el bucle, así como de los puntos donde se haya interrumpido. Los dos o más objetos generador que crremos de una misma función generador se ejecutarán, digámoslo así, en paralelo. Si estás familiarizado con los threads (hilos) que se emplean en lenguajes de alto nivel, como Java, podríamos considerarlos como diferentes hilos de ejecución (en realidad, es lo que son). Observa el siguiente listado:

El resultado que vemos en la consola es el siguiente:

Valor del primer generador: Object {value: 0, done: false}
Valor del primer generador: Object {value: 1, done: false}
Valor del primer generador: Object {value: 2, done: false}
Valor del segundo generador: Object {value: 0, done: false}
Valor del segundo generador: Object {value: 1, done: false}
Valor del primer generador: Object {value: 3, done: false}

Cómo ves, los valores del primer y el segundo generador, son independientes.

HECHO

Hasta ahora, siempre hemos visto el valor de la variable que nos devuelve yield, en la propiedad value del retorno. La propiedad done siempre nos devuelve false.

La propiedad done es un indicador de si el bucle se ha completado o no. Como estamos usando un bucle infinito, nunca se completará. Siempre existirá una iteración siguiente… a menos, claro, que se rompa realmente la ejecución del bucle con break. Observa el siguiente listado.

El resultado en consola es el siguiente:

Valor de miGenerador:  Object {value: 0, done: false}
Valor de miGenerador:  Object {value: 1, done: false}
Valor de miGenerador:  Object {value: undefined, done: true}
Valor de miGenerador:  Object {value: undefined, done: true}

Cómo ves, a partir de que se supera el valor 1 en contador se rompe el bucle (lo que ocurre en la línea 23). En ese momento, done vale true y, como ya no estamos en el proceso, la propiedad value contiene undefined. Sucesivas llamadas al método next() ya siempre arrojan el mismo resultado.

Esto nos puede servir como indicativo para saber si aún estamos dentro del bucle, o este ha finalizado. La forma de extraer las propiedades value y done tras una invocación a next() es mediante el uso de un punto. Observa el siguiente listado:

Como ves en la pantalla de tu navegador, el resultado es el mismo que en caso anterior, con la salvedad de que hemos extraido las propiedades value y done de forma independiente.

     

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *