Generadores: function* y yield para Secuencias Bajo Demanda
Aprende a crear generadores con function* y yield para generar secuencias de valores bajo demanda, implementar lazy evaluation y manejar iteraciones complejas de forma elegante.
TL;DR - Resumen rápido
- Los generadores son funciones especiales que pueden pausar y reanudar su ejecución
- Se definen con la sintaxis function* y usan yield para retornar valores
- Los generadores retornan un objeto iterador con métodos next(), return() y throw()
- Implementan lazy evaluation: los valores se generan solo cuando se solicitan
- yield* permite delegar la iteración a otro iterable o generador
Introducción a los Generadores
Los generadores son una característica poderosa de ES6 que permite crear funciones que pueden pausar su ejecución y reanudarla más tarde. A diferencia de las funciones regulares que ejecutan hasta el final y retornan un solo valor, los generadores pueden producir múltiples valores a lo largo del tiempo.
Esta capacidad de pausar y reanudar hace que los generadores sean ideales para trabajar con secuencias de datos, implementar lazy evaluation, y manejar flujos asíncronos complejos de forma más legible que con callbacks o promesas.
¿Por qué usar generadores?
Los generadores resuelven problemas comunes como la creación de secuencias infinitas, la iteración sobre estructuras de datos complejas, y la implementación de patrones de diseño como iteradores personalizados sin la verbosidad del protocolo de iteración manual.
Sintaxis Básica
Para crear un generador, usas la palabra clave function* (con el asterisco) en lugar de function. El asterisco indica que esta es una función generadora. Dentro del cuerpo del generador, usas yield para retornar valores y pausar la ejecución.
La Palabra Clave yield
yield es similar a return, pero con una diferencia fundamental: yield pausa la función y guarda su estado, permitiendo que se reanude más tarde desde el mismo punto. Cada vez que llamas a next() en el generador, la ejecución continúa desde el último yield.
Este ejemplo muestra un generador simple que produce tres números. Cada llamada a next() reanuda la ejecución desde el último yield, produciendo el siguiente valor. Cuando el generador termina, next() retorna done: true.
yield pausa la ejecución
A diferencia de return que termina la función, yield solo la pausa. El generador mantiene su estado (variables locales, posición de ejecución) y puede continuar desde donde se quedó cuando se llama a next() nuevamente.
El Objeto Generador
Cuando llamas a una función generadora, no ejecuta el cuerpo inmediatamente. En su lugar, retorna un objeto generador que implementa el protocolo de iterador. Este objeto tiene métodos para controlar la ejecución del generador.
Métodos del Generador
El objeto generador tiene tres métodos principales: next(), return() y throw(). Cada método tiene un propósito específico para controlar cómo el generador produce valores y maneja errores.
Este ejemplo muestra los tres métodos del generador. next() avanza la ejecución, return() termina el generador inmediatamente con un valor específico, y throw() inyecta un error en el generador que puede ser capturado con try/catch.
return() y throw() terminan el generador
Una vez que llamas a return() o throw() en un generador, este se considera cerrado. Llamadas posteriores a next() siempre retornarán done: true sin ejecutar más código del generador.
yield vs return
Es importante entender la diferencia entre yield y return en los generadores. Mientras que yield pausa la ejecución y permite que continúe más tarde, return termina el generador inmediatamente.
Este ejemplo muestra que return termina el generador inmediatamente. Aunque hay código después del return, nunca se ejecutará. El valor del return se incluye en el resultado de next(), pero done: true indica que el generador ha terminado.
Las diferencias clave entre yield y return son:
- yield pausa la ejecución y permite continuar más tarde
- return termina el generador inmediatamente
- yield puede usarse múltiples veces en un generador
- return solo se usa una vez (o implícitamente al final)
- El valor de return se incluye en el último resultado (done: true)
Delegación con yield*
yield* (con asterisco) es una forma especial de yield que delega la iteración a otro iterable o generador. Esto permite combinar múltiples generadores o iterar sobre estructuras anidadas de forma elegante.
Este ejemplo muestra cómo yield* delega la iteración a otro generador. Cuando usas yield* otherGenerator(), el generador actual produce todos los valores de otherGenerator antes de continuar con su propia ejecución.
yield* funciona con cualquier iterable
yield* no solo funciona con generadores, sino con cualquier iterable: arrays, strings, maps, sets, o cualquier objeto que implemente el protocolo de iteración. Esto lo hace muy flexible para combinar diferentes fuentes de datos.
Casos de Uso Prácticos
Los generadores son útiles en muchos escenarios reales. Desde secuencias infinitas hasta procesamiento de datos por lotes, los generadores ofrecen soluciones elegantes a problemas complejos.
Los generadores son perfectos para crear secuencias infinitas porque implementan lazy evaluation. Este ejemplo crea un generador de números pares infinito. Los valores se generan solo cuando se solicitan, sin consumir memoria infinita.
Este ejemplo muestra cómo usar generadores para procesar datos por lotes. El generador yield* toma un array y produce valores en grupos del tamaño especificado, lo cual es útil para procesar grandes conjuntos de datos sin sobrecargar la memoria. Los generadores implementan lazy evaluation: los valores se calculan solo cuando se solicitan, lo que es especialmente eficiente para secuencias grandes o infinitas.
Errores Comunes
Al trabajar con generadores, hay dos errores principales: olvidar usar yield en el cuerpo del generador, y reusar un generador que ya se cerró. Si el generador nunca yieldea un valor, next() retornará done: true inmediatamente. Una vez que un generador se cierra, no puede reabrirse y debes crear uno nuevo.
El ejemplo muestra ambos errores: un generador sin yield que termina inmediatamente, y un generador cerrado que no puede reabrirse. La solución en ambos casos es clara: usa yield para producir valores, y crea un nuevo generador cada vez que necesites iterar.
Error común: Reusar generadores
Los generadores son de un solo uso. Una vez que se completan, no pueden reiniciarse. Si necesitas iterar sobre los mismos valores múltiples veces, crea un nuevo generador cada vez llamando a la función generadora.
Resumen: Generadores
Conceptos principales:
- •Los generadores se definen con function* y usan yield para producir valores
- •Los generadores retornan un objeto iterador con next(), return() y throw()
- •yield pausa la ejecución y permite continuar más tarde
- •return termina el generador inmediatamente
- •yield* delega la iteración a otro iterable o generador
Mejores prácticas:
- •Usa generadores para secuencias infinitas o grandes (lazy evaluation)
- •Usa yield* para combinar múltiples iterables o generadores
- •Crea un nuevo generador para cada iteración, no reuses generadores
- •Usa try/catch en generadores para manejar errores con throw()
- •Usa generadores para procesamiento por lotes y streaming de datos