Generadores en PHP

Los generadores en PHP proporcionan una forma más sencilla de utilizar iteradores para recorrer estructuras agregadas

Contenido modificable

Si ves errores o quieres modificar/añadir contenidos, puedes crear un pull request. Gracias

Los iteradores en PHP son muy útiles pero a veces puede resultar tedioso a la hora de implementar las numerosas interfaces para crear el objeto a iterar. Desde PHP 5.5 están disponibles los generadores.

Un generador es como una función que actúa como un iterador y que devuelve un array. Un generador permite emplear foreach para iterar datos de forma que no se necesite cargar el array en la memoria o se requiera mucho tiempo para generarse. En su lugar se escribe una función generadora, que es como una función normal.

En lugar de usar la palabra return usa yield. Yield significa producir/ceder (en este contexto). Yield funciona de forma similar a return ya que devuelve un valor, pero en lugar de remover la función de la pila, yield guarda su estado, de esta forma puede continuar por donde estaba si se le llama otra vez. No es posible devolver un valor desde un generador pero si que se puede emplear return sin un valor para terminar su ejecución.

Por ejemplo en la función range() PHP tiene que generar un array con cada uno de los valores y devolverlo, lo que puede resultar en grandes arrays y consumir muchos recursos. Como alternativa se puede generar un generador xrange() que sólo necesitará memoria para crear un objeto Iterator y controlar el estado actual del generador.

function xrange($start, $limit, $step = 1){
    if ($start < $limit){
        if($step <= 0){
            throw new LogicException("Step tiene que ser positivo");
        }
        for ($i = $start; $i <= $limit; $i += $step){
            yield $i;
        }
    } else {
        if($step >= 0){
            throw new LogicException("Step tiene que ser negativo");
        }
        for ($i = $start; $i <= $limit; $i += $step){
            yield $i;
        }
    }
}

Tanto range() como xrange() producirán la misma salida:

echo "Números impares de una cifra con range():";
foreach(range(1, 9, 2) as $numero){
    echo "$numero, ";
}
echo "<br>";
echo "Numero impares de una cifra con xrange():";
foreach(xrange(1, 9, 2) as $numero){
    echo "$numero, ";
}

Cuando se llama a una función generator, devuelve un objeto con el que se puede iterar. Este objeto es de la clase Generator e implementa la interface Iterator de la misma forma en que lo hace un objeto iterador. Cuando iteras sobre ese objeto, PHP llama al generador cada vez que necesita un valor. El estado se guarda cuando el generador hace yield por lo que puede resumirse después cuando se necesite el próximo valor. Un ejemplo más sencillo:

function numeros(){
    echo "El generador ha empezado <br>";
    for ($i = 0; $i < 5; ++$i){
        yield $i;
        echo "Se ha hecho yield en $i <br>";
    }
    echo "El generador ha terminado <br>";
}
foreach(numeros() as $numero);
/*
Devuelve:
El generador ha empezado
Se ha hecho yield en 0
Se ha hecho yield en 1
Se ha hecho yield en 2
Se ha hecho yield en 3
Se ha hecho yield en 4
El generador ha terminado
*/

Los generadores no son un concepto nuevo, ya existen en C#, Python, JavaScript y Ruby (enumerators), y se suelen identificar con el uso de la palabra yield. Ahora vamos a emplear un ejemplo para recorrer las líneas de un archivo:

function lineasDeArchivo($nombreArchivo){
    $archivo = fopen($nombreArchivo, 'r');
    while (($linea = fgets($archivo)) !== false){
        yield $linea;
    }
    fclose($archivo);
}
foreach (lineasDeArchivo("archivo.txt") as $linea){
    echo $linea . "<br>";
}

La función generator abre el archivo y hace yield en cada línea del archivo cuando se le ordena. Cada vez que se llama a generator, continúa desde donde lo dejó. Cuando se han leído todas las líneas el generador termina junto con el loop.

Los iteradores en PHP están formados por parejas key/value. En el ejemplo anterior sólo se ha devuelto value, las keys eran numéricas (es así por defecto). Para devolver un array asociativo simplemente hay que usar la misma sintaxis que con los arrays asociativos:

foreach (lineasDeArchivo($nombreArchivo) as $key => $value) {
...
}

Pero yield no sólo devuelve valores, puede recibirlos también. Esto se hace mediante el método send() del objeto generator con el valor que quieras pasar. Este valor puede usarse para computar o para cualquier otra cosa:

function numeros(){
    for ($i = 0; $i < 10; ++$i){
        // Obtener un valor de llamada
        $cmd = (yield $i);
        if ($cmd == 'parar'){
            return; // Salir del generador
        }
    }
}
$generador = numeros();
foreach($generador as $valor){
    // Sólo queremos mostrar hasta 5
    if($valor == 5){
        $generador->send('parar');
    }
    echo "$valor, ";
}
// Devuelve: 0, 1, 2, 3, 4, 5

Los generadores son especialmente útiles para calcular grandes conjuntos de datos y no se quiere emplear excesiva memoria para mostrar todos al mismo tiempo o cuando no se sabe si se necesitarán todos los resultados. Debido a la forma en que se procesan los resultados con los generadores, el uso de memoria se puede reducir al mínimo asignando la memoria sólo al resultado actual.

Vamos a poner un ejemplo de la función file(), que devuelve todas las líneas de un archivo como un array, frente a la función que hemos creado antes, lineasDeArchivo():

// Usando lineasDeArchivo()
$memoria = memory_get_peak_usage();
foreach (lineasDeArchivo("archivo.txt") as $a);
echo memory_get_peak_usage() - $memoria . "<br>"; // 10400
// Usando file()
$memoria = memory_get_peak_usage();
foreach(file("archivo.txt") as $a);
echo memory_get_peak_usage() - $memoria . "<br>"; // 25536

Si se emplea con pocas líneas emplea un poco más de memoria lineasDeArchivo(), pero cuando el archivo comienza a tener un número de líneas considerable, file() aumenta mucho su uso y lineasDeArchivo() mantiene su valor.