Un generador es una función que contiene yield. Llamarla no ejecuta el cuerpo; devuelve un objeto Generator que foreach puede recorrer. Cada yield $value entrega un valor al bucle y pausa la función hasta que el bucle pide el siguiente, así que los valores se producen de uno en uno en lugar de reunirse antes en un array.
La salida alterna entre el generador y el bucle: cada valor se crea solo cuando el bucle lo pide.
Los generadores ahorran memoria
range(1, 1000000) construye un array de un millón de enteros antes de que empiece el bucle. Un generador que entrega los mismos números nunca tiene más de uno a la vez. Ejecútalo y compara la memoria que necesita cada versión:
Los dos bucles imprimen la misma suma, 500000500000, pero la versión con array indica extra memory 16.0 MB y el generador extra memory 0.0 MB. La diferencia crece con los datos. La misma idea es la que hace útiles los generadores con archivos grandes, resultados de bases de datos y páginas de una API: procesas un elemento, lo sueltas y tomas el siguiente.
Entregar claves y valores
yield $key => $value fija la clave que recibe foreach ($gen as $key => $value). Sin clave, un generador numera sus valores desde 0, como una lista. Un uso habitual es leer un archivo línea a línea con números de línea:
Solo hay una línea en memoria cada vez, así que la misma función maneja un archivo de log de 2 GB. fopen() y fgets() explican las funciones de archivos que se usan aquí.
Secuencias infinitas y pipelines perezosos
Un generador puede repetirse para siempre, porque solo avanza hasta donde le pide quien lo consume. Detenlo con break, o envuélvelo en otro generador que tome los primeros valores. Encadenar generadores pequeños da un pipeline en el que cada paso es perezoso:
fibonacci() nunca termina por sí solo, y aun así el programa acaba: take() deja de pedir después de la cantidad solicitada, y nada más allá llega a calcularse.
yield from: delegar en otro generador
yield from entrega cada valor de un array, de un iterador o de otro generador. Es la forma de dividir un generador en otros más pequeños, o de recorrer una estructura anidada de forma recursiva:
yield from conserva las claves interiores, así que los dos arrays entregan las claves 0 y 1. Después, iterator_to_array() sobrescribe a y b. Pasa false como segundo argumento siempre que solo importen los valores.
send() y getReturn()
Un generador también puede recibir valores. $x = yield; pausa y espera; $gen->send($value) reanuda el generador con $value como resultado de ese yield. Y un generador puede hacer return de un valor final, que lees con getReturn() cuando ha terminado.
La mayor parte del código nunca necesita send(); el uso diario es leer de forma perezosa con foreach. Es la pieza básica de las librerías de corrutinas.
Un generador solo se puede recorrer una vez
Los generadores avanzan y no se pueden rebobinar. Cuando un foreach ha consumido uno, un segundo bucle sobre el mismo objeto lanza un error:
La solución es llamar de nuevo a la función generadora en cada pasada, foreach (colors() as $c), o reunir los valores una vez con iterator_to_array(colors(), false) cuando de verdad los necesites dos veces. Si te encuentras haciendo eso a menudo, un array normal era la opción correcta para esos datos.
Preguntas frecuentes
¿Qué hace yield en PHP?
yield $value entrega un valor al código que recorre la función y pausa la función justo ahí. La siguiente vuelta del bucle la reanuda después del yield. Cualquier función que contenga yield devuelve un objeto Generator en lugar de ejecutarse cuando la llamas.
¿Por qué usar un generador en lugar de un array?
Un array guarda todos sus elementos en memoria a la vez; un generador produce un elemento cada vez. Recorrer range(1, 1000000) reserva el millón de enteros, mientras que un generador que los entrega con yield mantiene la memoria estable. Los generadores además permiten secuencias infinitas y dejan de trabajar en cuanto el bucle se rompe.
¿Qué es yield from en PHP?
yield from $source entrega cada valor de otro generador, de un array o de un Traversable, como si el generador actual los hubiera entregado él mismo. Además se evalúa como el valor de retorno del generador interior.
¿Puedo recorrer un generador dos veces?
No. Un generador solo avanza. Cuando un foreach ha terminado, volver a recorrerlo lanza Exception: Cannot traverse an already closed generator. Llama de nuevo a la función generadora para obtener un generador nuevo, o guarda los valores con iterator_to_array() si los necesitas dos veces.
¿Cómo convierto un generador en un array?
iterator_to_array($gen) reúne todos los valores, conservando las claves del generador. Si las claves se repiten, por ejemplo después de un yield from, los valores posteriores sobrescriben a los anteriores, así que pasa false como segundo argumento para obtener una lista simple: iterator_to_array($gen, false).