Rápida introducción a los Generadores en JavaScript

Rápida introducción a los Generadores en JavaScript

El uso de generadores en la codificación diaria no es tan común como otras características en JavaScript.

A pesar de su utilidad, no es frecuente encontrarlos en la mayoría de los códigos que revisamos y literalmente este artículo se me ha ocurrido porque estaba escribiendo una función para una migración entre dos sistemas y al ser una cantidad de registros considerables, pues decidí explorar los generadores ya que en su uso, aunque no tan fáciles de comprender, ofrecen un características que ayudan a mejorar el desempeño de las aplicaciones.

¿Cuál es la razón, será la falta de comprensión o se subestiman sus beneficios?

Desde ECMAScript 2015, JavaScript nos ofrece una herramienta particular: los generadores. Estos permiten controlar la programación asíncrona, crear objetos iterables y producir múltiples valores. Veamos cómo funcionan y cómo pueden ser útiles.

¿Qué son los Generadores?

Los generadores se distinguen de las funciones tradicionales porque pueden comenzar y detener su ejecución en múltiples ocasiones. Esto les permite producir varios valores y retomar su ejecución más tarde, lo que es ideal para operaciones asíncronas, construcción de iteradores y manejo de flujos de datos continuos.

Un generador se identifica por la sintaxis function*. Veamos un ejemplo simple:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

En este caso, yield devuelve un valor y detiene la ejecución del generador. Con cada llamada, el generador produce el siguiente valor.

Interacción con objetos Generadores

Al invocar una función generadora, no se ejecuta directamente su contenido. En lugar de eso, devuelve un objeto Generador que nos permite controlar su ejecución. Dado que este objeto es iterable, es compatible con bucles for...of y operaciones similares.

Desglosemos el objeto Generador:

  • next(): Este método reanuda el generador, devuelve el siguiente valor producido y muestra si el generador ha terminado mediante la propiedad done.

    Ejemplo:

    console.log(generator.next()); // { value: 1, done: false }
  • return(): Este método finaliza el generador anticipadamente, como si se hubiera ejecutado un comando return.

    Ejemplo:

    console.log(numbers.return(100)); // { value: 100, done: true }
  • throw(): Nos permite introducir un error, facilitando la gestión de errores directamente en el generador.

    Ejemplo:

    function* generateTasks() {
    try {
      yield 'Iniciar tarea';
      yield 'Continuar tarea';
      yield 'Casi terminado';
    } catch (error) {
      console.log('Se produjo un problema:', error.message);
    }
    }
    
    const tasks = generateTasks();
    
    console.log(tasks.next().value); // Salida: 'Iniciar tarea'
    console.log(tasks.next().value); // Salida: 'Continuar tarea'
    tasks.throw(new Error('¡Vaya! Algo salió mal.')); 
    // Salida: 'Se produjo un problema: ¡Vaya! Algo salió mal.'
    console.log(tasks.next()); // Salida: { value: undefined, done: true }

Generadores para flujos de datos infinitos

Los generadores son herramientas excepcionales en JavaScript que se prestan para gestionar flujos de datos que potencialmente no tienen un final definido. Esta característica es especialmente útil en escenarios donde se requiere un flujo constante de datos, como las transmisiones en tiempo real, los feeds de noticias actualizables o las secuencias de números generadas algorítmicamente.

La belleza de los generadores radica en que, en lugar de cargar o calcular todos los datos de antemano (lo que podría ser impracticable o ineficiente para flujos infinitos), los generadores producen datos "bajo demanda". Esto significa que los valores se calculan y se entregan sólo cuando se soliciten, permitiendo una gestión eficiente de los recursos y un control detallado sobre la producción de datos.

Ejemplo:

function* infiniteCounter() {
    let count = 0;
    while (true) {
        yield count++;
    }
}

El generador anterior producirá un contador infinito, empezando desde cero y aumentando indefinidamente, pero solo entregará un nuevo número cuando se solicite.

Iteración Síncrona y Asíncrona con Generadores

Los generadores no sólo son útiles para la iteración síncrona; cuando se combinan con promesas, se convierten en herramientas poderosas para manejar operaciones asíncronas. Pueden simular el patrón async/await, proporcionando un mecanismo para escribir código asíncrono de manera más limpia y estructurada.

La capacidad de "pausar" y "reanudar" dentro de un generador lo hace ideal para esperar la resolución de una promesa antes de continuar con la siguiente iteración. Esto permite un flujo de control más intuitivo y legible, especialmente cuando se manejan múltiples operaciones asíncronas en secuencia o se necesita un manejo de errores más granular.

Ejemplo:

function* asyncGenerator() {
    const data1 = yield fetch('https://api.example.com/data1').then(response => response.json());
    console.log(data1);
    const data2 = yield fetch('https://api.example.com/data2').then(response => response.json());
    console.log(data2);
}

En el ejemplo anterior, el generador pausará su ejecución para esperar la respuesta de la primera solicitud fetch. Una vez que se resuelva la promesa, continuará y hará lo mismo con la segunda solicitud.

Este enfoque, aunque es un poco más manual que el patrón async/await, proporciona una gran flexibilidad, permitiendo a los desarrolladores controlar con precisión cuándo y cómo se manejan las operaciones asíncronas.

Caso Práctico: Números Fibonacci

Supongamos que deseas calcular los números de Fibonacci. Los números de Fibonacci son una secuencia en la que cada número es la suma de los dos anteriores: 0, 1, 1, 2, 3, 5, 8, 13, ...

Versión "Normal" (sin Generadores)

function getFibonacciNumbers(n) {
    let numbers = [0, 1];
    for (let i = 2; i < n; i++) {
        numbers.push(numbers[i - 1] + numbers[i - 2]);
    }
    return numbers;
}

const first100Fib = getFibonacciNumbers(100);
console.log(first100Fib);

Esta función genera los primeros n números de Fibonacci y los almacena todos en un array. Si n es un número muy grande, este método puede consumir una gran cantidad de memoria.

Versión con Generadores

function* generateFibonacci() {
    let [prev, curr] = [0, 1];
    while (true) {
        yield prev;
        [prev, curr] = [curr, prev + curr];
    }
}

const fibGenerator = generateFibonacci();
let first100FibWithGenerator = [];
for (let i = 0; i < 100; i++) {
    first100FibWithGenerator.push(fibGenerator.next().value);
}
console.log(first100FibWithGenerator);

Con el generador, no calculamos todos los números de Fibonacci al inicio. En lugar de eso, producimos cada número cuando se necesita. Esto significa que podríamos producir un número infinito de números de Fibonacci sin agotar la memoria, ya que solo calculamos y almacenamos un número en un momento dado.

Diferencias y Ventajas:

  1. Eficiencia de memoria: El generador no almacena todos los números en memoria, lo que lo hace más eficiente en términos de uso de memoria, especialmente cuando se trata de grandes volúmenes de datos.

  2. Flexibilidad: Puedes decidir cuántos números de Fibonacci generar en tiempo de ejecución. Con la versión "normal", tendrías que definirlo antes de ejecutar la función.

  3. Producción bajo demanda: Con el generador, puedes generar valores bajo demanda. Puedes generar 100, 1000 o cualquier número de valores de Fibonacci cuando lo necesites.

Este es un ejemplo simplificado, pero ilustra el punto clave de los generadores: la capacidad de producir valores bajo demanda sin consumir toda la memoria. Especialmente en aplicaciones con flujos de datos en tiempo real o conjuntos de datos muy grandes, los generadores pueden ofrecer un rendimiento y una flexibilidad significativamente mejores.

Conclusión

Los generadores en JavaScript ofrecen una herramienta eficaz para tareas asíncronas y creación de objetos iterables. La próxima vez que necesites procesar datos de manera dinámica, considera usar generadores.

Sería interesante conocer ejemplos de situaciones en las que los generadores hayan sido útiles en escenarios reales. Cuantos más casos veamos, más fácil será identificar en qué contextos son más adecuados.

Happy coding! :D


Photo by Markus Spiske on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?