Generators no PHP 5.5

Artigo que apresenta o funcionamento das funções generators, introduzida no PHP 5.5 para facilitar a criação de iteradores.

Introdução

Uma das mais importantes novidades do PHP 5.5 foi o suporte aos generators. Esta novidade permite a criação de alguns tipos de iteradores de forma muito mais fácil e intuitiva.

Já comentei sobre iteradores no artigo As interfaces Iterator, ArrayAccess e Countable. Porém, vou apresentar rapidamente o conceito para quem ainda não o conhece.

Iteradores: as vantagens e os inconvenientes

Iterador é um mecanismo que permite que um objeto seja usado em um loop com iterações. No PHP, isso é possível desde a versão 5, em que foi criada a interface Iterator.

Ao criar uma classe que implementa a interface Iterator, é possível passar um objeto desta classe para, por exemplo, uma estrutura foreach. A classe é que controlaria quais elementos são retornados a cada iteração e quando o "cursor" interno do objeto deve avançar ou recuar. Como a classe que define de onde os elementos são consultados para serem retornados, este recurso pode ser usado para poupar memória. Por exemplo, ao invés de armazenar todos os elementos em um array e percorrê-lo, você pode criar um iterador que devolve um por um (sob demanda) obtendo os valores de um socket, de um arquivo, ou outra estrutura. Desta forma, pode ficar apenas um elemento na memória, por iteração.

O único inconveniente dos iteradores, é a necessidade de implementar 5 métodos:

  • current - para retornar o elemento corrente (e não avançar o cursor)
  • key - para obter a chave corrente (e não avançar o cursor)
  • next - para avançar o cursor para o próximo elemento
  • rewind - para retornar o cursor para o início
  • valid - para checar se a posição atual existe

Implementação de funções generator

Com a introdução do suporte a generators, é possível criar um iterador apenas com uma função ou um método de uma classe. Para criar uma função generator, basta que ela possua a palavra reservada "yield". O yield é como um return especial, que retorna uma chave/valor e, quando for solicitado o próximo elemento do iterador, a função continua de onde parou o último yield.

Veja um exemplo bem simples de função generator:

function colecao() {
    yield 'Burnout Paradise';
    yield 'Motorstorm Apocalypse';
    yield 'Max Payne';
    yield 'Shadow of the Colossus';
    yield 'Super Street Fighter';
}

foreach (colecao() as $valor) {
    echo $valor . PHP_EOL;
}

A função colecao, no exemplo acima, irá retornar um objeto da classe Generator, que implementa automaticamente a interface Iterator. Com isso, ela pode ser usada em um foreach. A cada vez que o foreach solicita um novo elemento, a função generetor já sabe que precisa continuar a execução desde o último yield.

Mas a situação mais comum não será uma função generator ter várias chamadas de yield. Veja um exemplo de função generator que devolve um elemento de cada vez, mas usando um único yield:

function getLinhasArquivo($arquivo) {
    $handle = fopen($arquivo, 'r');
    if (!$handle) {
        return;
    }
    while (!feof($handle)) {
        $linha = fgets($handle);
        yield $linha;
    }
    fclose($handle);
}

E para retornar uma chave e um valor, basta usar a seta dupla, conforme exemplo abaixo:

function colecao() {
    yield 'a' => 37489;
    yield 'b' => 1873;
    yield 'c' => 81391;
}

Observação: caso um yield omita a chave, ela é retornada automaticamente com base em uma sequência numérica iniciada em 0 (zero). Caso o valor também seja omitido, ele é considerado null.

Atenção: caso uma função generetor retorne null (via comando return), significa que o iterador chegou ao final. Consequentemente, as chamadas de yield posteriores ao retorno nulo não são considerados. Por outro lado, retornar null pelo yield significa que o elemento retornado pelo iterador é null. Por exemplo:

function colecao() {
    yield 'a';   // iterador retorna "a"
    yield null;  // iterador retorna null
    yield 'b';   // iterador retorna "b"
    return null; // iterador encerrado
    yield 'c';
}

O iterador retornado pela função acima irá devolver os seguintes elementos: "a", null e "b". Ele nunca irá retornar "c".

Observação: os objetos gerados pelas funções generators, sendo derivados da classe Iterator, possuem o método rewind implementado. Porém, nestes objetos, este método emite uma exceção, caso a função generator já tenha sido iniciada. Ou seja, numa situação convencional, você só consegue percorrer um objeto da classe Generator uma única vez. Para percorrer novamente, é necessário criar outro objeto.

Considerações Finais

Generators não vieram para substituir iteradores, apenas para tornar mais fácil a criação de alguns tipos de iteradores.

Quando se trabalha com coleções de dados, é sempre importante fazer uma análise sobre a real necessidade de acesso randômico aos elementos e se eles ocupam muito espaço em memória. Caso ocupam muito espaço em memória e não precisam de acesso randômico (apenas precisam de acesso sequencial), então a utilização de um iterador pode ser viável.

7 comentários