Para que serve o Yield?
Vou demonstrar em mais de uma linguagem para servir para mais pessoas.
Uma tradução boa para a palavra yield seria "produz". Também pode ser "gera", "entrega", "colhe", "coleta" ou "fornece".
No fundo, geradores não são para economizar memória. Isto é mais um efeito colateral bem-vindo. Eles existem para criar sequências de dados sob demanda.
A ideia geral dele é poder controlar melhor o que acontece entre a geração de cada elemento da sequência.
Uma das vantagens é produzir código com melhores abstrações. Você encapsula o mecanismo de como se obtém o dado dentro de uma função e deixa outra função, provavelmente em um loop, manipular estes dados sem se preocupar em como ele foi obtido. Você separa o mecanismo da regra de negócio.
Ele também permite o que se chama lazy evaluation em que você só exerce a computação de um item da sequência quando e se realmente você vai usá-lo. Sem o gerador é muito comum gerar sequências de dados enormes, provavelmente em listas ou arrays, e gastar um tempo enorme para gerar todos os elementos e depois só usar de fato alguns deles e descartar o resto. O gerador economiza memória e processamento.
Uma outra utilização é na criação de máquina de estados e corrotinas já que ele consegue manter estado entre as chamadas de suas execuções. Ele tem pausas naturais.
O funcionamento de um gerador é mais ou menos igual em todas as linguagens que o possuem.
O ganho em memória é relativo. Se você precisar de toda a sequência gerada e usada ao mesmo tempo, não há ganho (se souber o que está fazendo).
Geradores não são ótimos para todos os casos. Eles provocam em pouco de overhead. Não que isto seja motivo para não usar, mas se não fornecer uma real vantagem não tem porque aceitar um overhead.
Coloquei no GitHub para referência futura.
Python
O Python tem especificidades próprias mas essencialmente em todas linguagens funcionam igual. Ele cria um generator, ou seja, cria uma lista de dados que vão sendo consumidos sob demanda. Em geral é usado para dar melhores abstrações ao código. Tudo que se faz com ele, dá para fazer sem ele de forma muito semelhante, mas expondo o mecanismo de geração dos dados.
Ele retorna um valor mantendo o estado de onde parou. Quando executa de novo ele continua de onde parou. Ele controla o estado de um enumerador entre execuções da função.
def impar(elems):
for i in elems:
if i % 2:
yield i
for x in impar(range(1000)):
Este código imprimirá todos os ímpares de 0 à 1000. Ele vai chamar o método 500 vezes, cada vez trará um número ímpar. O compilador/biblioteca montará a estruturá interna para saber em que número ele está em cada chamada.
Claro que tem uma variável escondida que sobrevive além do ambiente interno da função. Então esse i
não começa de novo em cada chamada da função. Note que você pode chamar a função impar()
sem saber como ela faz a seleção internamente.
Este exemplo é simples e óbvio, mas pense em coisas mais complexas. Você abstrai melhor. Você diz que vai chamar um método que filtra os ímpares para você. Não interessa como. O que você vai fazer com a informação gerada é problema seu. A função com o yield
tem responsabilidade única de gerar a informação.
Outro exemplo:
def numeros():
yield 1
yield 2
yield 3
print numeros()
print numeros()
print numeros()
Isto imprimirá 1, 2 e 3.
JavaScript
A especificação ECMAScript 6 acrescentou iterators e generators ao núcleo duro da linguagem, porém hoje (final de 2015) ainda há limitações de suporte nos browsers. Vou explicar como funciona/vai funcionar, para que já fiquemos preparados para o futuro :)
##Iterators
Um iterator em ES6 é simplesmente um objeto que implementa um método next
, responsável por retornar o próximo item da sequência – um objeto com as propriedades done
e value
. Não há sintaxe nova, o que foi criado é simplesmente um protocolo. Exemplo da MDN:
function makeIterator(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
}
}
var it = makeIterator(['yo', 'ya']);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done); // true
Em PHP, um iterator pode ser enumerado por um foreach
, porque ele também é Enumerable
. Em ES6 existe o for..of
, porém ele não enumera iterators, e sim iterables, que são objetos que implementam @@iterator – ou seja, possuem um método com a chave Symbol.iterator
. Esse método retorna um iterator:
function makeIterable(array){
var nextIndex = 0;
var it = {};
it[Symbol.iterator] = function() {
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
};
return it;
}
for(let val of makeIterable(['yo', 'ya'])) console.log(val);
A maneira mais prática de criar um iterable é usar uma função geradora (que já vamos discutir mais adiante):
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
for(let val of myIterable) {
console.log(val)
}
Um iterator pode ser também iterable, o que é mais próximo do que acontece no PHP:
function makeIterableIterator(array){
var nextIndex = 0;
var it = {};
it[Symbol.iterator] = function() {
return this;
};
it.next = function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
};
return it;
}
for(let val of makeIterableIterator(['yo', 'ya'])) console.log(val);
É mais ou menos isso que fazem os iterables que são parte do núcleo da linguagem, como Array
, Map
, Set
etc.
Para saber mais sobre iterables, recomendo a leitura de Iterables and iterators in ECMAScript 6, onde há muitos exemplos.
##Generators
As funções geradoras facilitam a criação de iterables. São basicamente funções com a capacidade de suspender sua execução, e mesmo assim manter o estado para a execução seguinte. O ES6 introduziu uma nova sintaxe para isso, function*
. Exemplo simples da MDN:
function* idMaker(){
var index = 0;
while(true)
yield index++;
}
var gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
O gerador criado por idMaker
é ao mesmo tempo um iterator e um iterable. A "mágica" fica por conta do yield
, que é quem suspende a execução. Cada vez que o next
é executado, a função retoma de onde parou, com seu escopo local intacto.
Basicamente, os generators permitem a criação de sequências computadas de maneira "preguiçosa", um valor por vez, conforme os valores são necessários. Eles podem ainda ser combinados com promessas para que seja possível tratar operações assíncronas de maneira semelhante, podem ser encadeados recursivamente, e podem ser trocar dados entre si, permitindo a implementação de co-rotinas.
No comentário abaixo eu coloco sobre C# e apresento mais detalhes.