Introdução a generators (em JavaScript)
Mesmo após anos de experiência com programação, generator talvez tenha sido a ideia/conceito de programação que mais demorei a entender. Gosto de pensar que parte do motivo vem da forma como a mensagem é geralmente transmitida se tratando do assunto. Dito isso, a proposta desse artigo é tentar explicar generators de forma simples.
Anatomia
function* doSomething() {
...
yield someVariable;
...
}
A primeira coisa que deve ter notado é que, em sintaxe, generators se parecem bastante com funções. A diferença é que para definir um generator, precisamos usar a keyword function*
.
Bem que poderia haver uma keyword
generator
, né? :p
Você também deve ter notado a keyword yield
. Essa é um pouco mais complicada de explicar. O que posso dizer por enquanto é que uma das traduções diretas de yield é "produzir".
Comportamento
Para entender como os generators se comportam, vamos compará-los a uma função comum. Veja como geraríamos ou produziríamos IDs sequenciais sem o uso de generators:
let id = 0;
function generateId() {
return ++id;
}
generateId(); // 1
generateId(); // 2
Nesse caso, o estado da nossa função fica fora de seu escopo. Em outras palavras, as variáveis necessárias para a função são definidas fora da definição dela própria. Isso é óbvio, já que se declarassemos a variável id
dentro da definição da função, o valor dessa variável seria reiniciado a cada execução.
Porém, isso não é necessariamente verdade para generators porque o ciclo de vida deles pode durar mais de uma execução.
Isso é possível por conta de duas coisas:
-
A keyword
yield
Pense nela como uma
return
especial. A diferença é que ayield
retorna o valor da expressão à sua frente (assim como areturn
) sem finalizar a função. -
O retorno
Diferente de funções, generators sempre retornam um objeto
Generator
. Esse objeto possui um método.next()
que é capaz de reexecutar a função partindo do últimoyield
até o próximo.
Vamos então vamos reescrever o gerador de IDs usando um generator:
function* generateId() {
let id = 0;
yield ++id;
yield ++id;
return; // Desnecessário nesse caso
}
const idGenerator = generateId();
idGenerator.next().value; // 1
idGenerator.next().value; // 2
No entanto, esse generator tem uma limitação: ele só é capaz de gerar dois IDs. Então vamos refatorá-lo para que possa gerar infinitos IDs:
function* generateId() {
let id = 0;
while (true) {
yield ++id;
}
}
const idGenerator = generateId();
idGenerator.next().value; // 1
idGenerator.next().value; // 2
Devido ao comportamento dos generators, pudemos por while (true)
(sem bloquear o event loop) para sempre termos um novo yield
e agora ele pode gerar infinitos IDs!
Isso não é lindo?!
Conclusão
Isso foi só a ponta do iceberg! Propositalmente deixei algumas "pontas soltas" para instigar a sua curiosidade, como "o que acontece ao executar .next()
depois de todos os yield
do generator serem retornados?" ou "para que serve a keyword return
em um generator?".
Para a resposta dessas perguntas e explicação de todos os outros casos que não citei aqui, você pode consultar o artigo dedicado da MDN.
Espero que eu tenha te ajudado a avançar pelo menos um pouquinho no seu entendimento sobre generators. Abraços!
Complementando...
Vale lembrar que cada vez que vc chama a função geradora, um novo generator é criado:
function *generateId() {
let id = 0;
while (true) {
yield++id;
}
}
// cria um generator e extrai alguns valores
const gen = generateId();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
// cria outro generator (independente do primeiro)
const outroGen = generateId();
console.log(outroGen.next().value); // 1
console.log(outroGen.next().value); // 2
// um não interfere no outro
console.log(gen.next().value); // 4
console.log(outroGen.next().value); // 3
E como um Generator
é iterável, posso usá-lo em um for..of
:
// coloca um limite máximo para o id
function *generateId(maxId) {
let id = 0;
while (id < maxId) {
yield ++id;
}
}
for (const id of generateId(10))
console.log(id); // imprime os números de 1 a 10
Claro, se usar em um for
, tem que tomar cuidado quando o generator é infinito (neste caso, dentro do for
tem que controlar a condição de parada e usar break
, por exemplo).
Por fim, outra forma de pensar em um generator é imaginá-lo como uma função que pode ser "pausada". Cada vez que ela encontra um yield
, o valor é retornado, e ela fica pausada, esperando até que o próximo valor seja pedido (e só aí ela volta a executar, até encontrar o próximo yield
, ou até chegar ao final da função, que é quando ele se encerra).
Outro detalhe é que um generator só pode ser percorrido uma vez. Se precisar iterá-lo mais de uma vez, ou cria-se outro, ou guarda-se os valores em outra estutura (no exemplo acima, poderia fazer const valores = [...generateId(10)]
para guardar os valores em um array).
Boa já tinha estudado sobre o assunto e o funcionamento até que é fácil de entender só que ainda não consegui aplica-lo na vida real. Teria algum exemplo de aplicação utilizando em uma aplicação real?