Um generator é uma função que contém yield. Chamá-la não executa o corpo; ela retorna um objeto Generator que o foreach consegue percorrer. Cada yield $value entrega um valor ao loop e pausa a função até o loop pedir o próximo, então os valores são produzidos um de cada vez em vez de serem reunidos num array primeiro.
A saída alterna entre o generator e o loop: cada valor só é produzido quando o loop o pede.
Generators economizam memória
range(1, 1000000) monta um array de um milhão de inteiros antes de o loop começar. Um generator que entrega os mesmos números nunca guarda mais de um deles. Rode e compare a memória de que cada versão precisa:
Os dois loops imprimem a mesma soma, 500000500000, mas a versão com array informa extra memory 16.0 MB e o generator extra memory 0.0 MB. A diferença cresce com os dados. A mesma ideia torna os generators úteis para arquivos grandes, resultados de banco de dados e páginas de APIs: processe um item, descarte-o, pegue o próximo.
yield com chaves e valores
yield $key => $value define a chave que foreach ($gen as $key => $value) recebe. Sem chave, um generator numera os valores a partir de 0, como uma lista. Um uso comum é ler um arquivo linha por linha com os números das linhas:
Só uma linha fica na memória por vez, então a mesma função lida com um arquivo de log de 2 GB. fopen() e fgets() explicam as funções de arquivo usadas aqui.
Sequências infinitas e pipelines preguiçosos
Um generator pode rodar para sempre, porque só avança até onde o consumidor pede. Pare com break, ou envolva-o em outro generator que pega os primeiros valores. Encadear pequenos generators dá um pipeline em que cada etapa é preguiçosa:
fibonacci() nunca termina sozinho, mas o programa termina: take() para de pedir depois da quantidade pedida, e nada além disso é calculado.
yield from: delegar a outro generator
yield from repassa cada valor de um array, de um iterator ou de outro generator. É como você divide um generator em menores, ou percorre uma estrutura aninhada de forma recursiva:
yield from mantém as chaves internas, então os dois arrays entregam as chaves 0 e 1. O iterator_to_array() então sobrescreve a e b. Passe false como segundo argumento sempre que só os valores importarem.
send() e getReturn()
Um generator também pode receber valores. $x = yield; pausa e espera; $gen->send($value) retoma o generator com $value como resultado daquele yield. E um generator pode dar return num valor final, que você lê com getReturn() depois que ele terminou.
A maior parte do código nunca precisa do send(); ler de forma preguiçosa com foreach é o uso do dia a dia. Ele é a peça básica por trás das bibliotecas de corrotinas.
Um generator só pode ser percorrido uma vez
Generators andam para a frente e não podem ser rebobinados. Depois que um foreach consumiu um, um segundo loop sobre o mesmo objeto lança uma exceção:
A correção é chamar a função do generator de novo a cada passada, foreach (colors() as $c), ou reunir os valores uma vez com iterator_to_array(colors(), false) quando você realmente precisar deles duas vezes. Se você se pegar fazendo isso com frequência, um array simples era a escolha certa para aqueles dados.
Perguntas frequentes
O que faz o yield no PHP?
yield $value entrega um valor ao código que percorre a função e pausa a função ali mesmo. A próxima iteração do loop a retoma depois do yield. Qualquer função que contenha yield retorna um objeto Generator em vez de rodar quando você a chama.
Por que usar um generator em vez de um array?
Um array guarda todos os elementos na memória ao mesmo tempo; um generator produz um elemento por vez. Percorrer range(1, 1000000) aloca todos o milhão de inteiros, enquanto um generator que os entrega com yield mantém a memória estável. Generators também permitem sequências infinitas e param o trabalho cedo quando o loop sai com break.
O que é yield from no PHP?
yield from $source repassa cada valor de outro generator, de um array ou de um Traversable, como se o generator atual os tivesse entregado. Ele também tem como valor o retorno do generator interno.
Posso percorrer um generator duas vezes?
Não. Um generator só anda para a frente. Depois que um foreach terminou, percorrer de novo lança Exception: Cannot traverse an already closed generator. Chame a função do generator de novo para ter um novo generator, ou guarde os valores com iterator_to_array() se precisar deles duas vezes.
Como converto um generator num array?
iterator_to_array($gen) reúne todos os valores, mantendo as chaves do generator. Se as chaves se repetem, por exemplo depois de um yield from, os valores posteriores sobrescrevem os anteriores, então passe false como segundo argumento para ter uma lista simples: iterator_to_array($gen, false).