Generadores PHP y por qué son tan útiles

Generadores PHP y por qué son tan útiles

Muchas veces necesitamos manejar gran volumen de datos que, cuando son cargados en memoria pueden reducir el rendimiento de nuestra aplicación, o incluso, arrojar una excepción al superar la máxima memoria disponible.

Para mostrar la importancia de la carga en memoria, utilizaremos el siguiente ejemplo.

Definamos la función numbers() que cree una lista de números enteros desde el 1 hasta el límite que le asignemos:

function numbers(int $limit) {
    $numbers = [];
    for ($i = 1; $i <= $limit; $i++) {
        $numbers[] = $i;
    }
  
    return $numbers;
}

Acto seguido, utilizaremos esta función para imprimir la lista completa. En este caso será hasta el 20:

foreach (numbers(20) as $range) {
    echo "Number: {$range}" . PHP_EOL;
}

Teniendo como resultado:

Screenshot-2021-12-23-at-23.47.30.png

¿Qué pasaría si incrementamos el límite considerablemente? Utilicemos ahora la constante PHP_INT_MAX, que viene a ser el máximo entero soportado por tu versión de PHP. En mi caso: 9223372036854775807. Probemos:

Screenshot-2021-12-23-at-23.52.50.png
wtf.gif

¿Qué pasó? Pues que el arreglo creado por numbers() fue demasiado grande. Al tener que ser cargado en memoria el servidor arroja una excepción pues supera el el límite establecido.

Podemos arreglar esto incrementando el límite de memoria utilizado por nuestro servidor, sin embargo.. ¿eso no sería una solución muy eficiente, cierto? 🤔 Veamos una alternativa.

Entrando en escena: Generadores

Convirtiendo nuestra función numbers() en un generador nos quedaría de la siguiente forma:

function numbers(int $limit) {
    $numbers = [];
    for ($i = 1; $i <= $limit; $i++) {
        yield $i;
    }
}

Como puedes notar, ahora empleamos el keyword yield, luego entraremos en detalle.

Si volvemos a correr nuestra función para imprimir los valores podemos notar que ahora nuestro programa sí es capaz de procesarlo (va a tardar un buen rato eso sí):

Screenshot-2021-12-24-at-00.22.10.png
celebration.gif

Generadores PHP

Los generadores son constructores poderosos que fueron incluidos en PHP 5.5. Estos nos permiten iterar sobre datos sin necesidad de cargarlos en memoria. Además, a diferencia de las funciones comunes, los generados pueden retornar múltiples valores.

Si te preguntas sobre cómo es posible que no utilicen demasiada memoria, la respuesta es simple: Los generadores almacenan su estado actual.

Las funciones regulares retornan un único valor: Luego del primer return, todas las sentencias siguientes son ignoradas:

function data() {
     return 10;
     return 200;
     return 3000;
}

dump(data()); //  10

Como puedes ver en el ejemplo, luego del primer return la función es terminada. Si quisiéramos retornar múltiples valores, podemos utilizar yield en lugar de return:

function data() {
     yield 10;
     yield 200;
     yield 3000;
}

dump(data());

Pero notarás que el valor retornado por dump() no es un arreglo, si no un objeto Generator:

Generator {
    executing: {...}
    closed: false
}

Lo importante por notar acá es que cada vez que utilicemos la palabra yield, PHP tratará la función de una manera totalmente distinta a las funciones regulares. La tratará como un generador. De hecho, PHP no ejecutará ninguna sentencia dentro del generador hasta que empecemos a recorrer el generador.

Los generadores nos brindan métodos tales como current() , que nos trae la iteración actual, y next() que procesa la siguiente iteración.

Puedes ver todos los métodos de esta clase en esta sección de la documentación.

Probemos:

$generator = data();

$first = $generator->current();
$generator->next();
$second = $generator->current();

dump($first); // 10
dump($second); // 200

Ejemplo real

Hace algún tiempo, tuve la necesidad de tener que procesar un CSV gigante para poder buscar una línea en específico. Estamos hablando de un archivo de casi 2GB, que si bien no parece demasiado, sí lo es cuando interactuamos con este.

Si lo hubiera hecho de manera tradicional, habría tenido que cargar el archivo en mi programa, para luego recorrerlo hasta encontrar el valor deseado. Esto conllevó a que mi primer intento fracase puesto que alcanzaba la máxima cantidad de memoria utilizada por mi app. Entonces fue cuando en lugar de cargar todo el archivo en memoria para luego recorrerlo, decidí por optar por leer solo 1 línea a la vez de la mano de un generador.

¿El resultado? Una función brutalmente eficiente que -virtualmente- no utiliza apenas memoria 😎.

Cierre

Como puedes notar, los generadores son muy útiles para poder manejar grandes volúmenes de datos sin comprometer la memoria de nuestro servidor. Algo muy útil para lectura/escritura de archivos o extracción y manejo de valores en grandes bases de datos.

Laravel trae ahora soporte nativo de generadores mediante la clase Lazy Collection, puedes leer sobre ello en este otro artículo.

2019-2025 © kennyhorna.com | @kennyhorna