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 a yield retorna o valor da expressão à sua frente (assim como a return) 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 último yield 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).

Muito bacana a explicação, acho que só faltou um caso de uso mais "mão na massa", eu aprendi sobre generators em um curso de JS e fiquei absolutamente indignado de como algo tão poderoso simplesmente passa em branco no dia a dia, são raros os tutoriais sobre o assunto e aplicações no dia a dia.
Seguem alguns artigos explicando casos de uso de um *generator*: - https://dev.to/rfornal/use-cases-for-javascript-generators-1npc - https://jrsinclair.com/articles/2022/why-would-anyone-need-javascript-generator-functions/ - https://itnext.io/a-quick-practical-use-case-for-es6-generators-building-an-infinitely-repeating-array-49d74f555666
Show, parabéns pela simplicidade da explicação já cheguei a olha sobre , mas sai meio perdido no início,ai fui olhar de novo ficou claro,mas ainda não apliquei.

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?

Realmente, Renato, é meio complicado pensar em aplicações pra isso porque é meio que um paradigma diferente! Mas te respondendo: isso poderia ser usado para processar coisas pesadas compassadamente, como ler um arquivo de texto gigante linha-a-linha economizando memória. Ou pra implementar funções com throttling ou debouncing. Até mesmo criar funções "canceláveis"! E por aí vai... A galera do [Redux Saga](https://redux-saga.js.org/) utiliza generators como feature base pra o funcionamento da lib. Vale a pena dar uma conferida.
Seguem alguns artigos explicando casos de uso de um *generator*: - https://dev.to/rfornal/use-cases-for-javascript-generators-1npc - https://jrsinclair.com/articles/2022/why-would-anyone-need-javascript-generator-functions/ - https://itnext.io/a-quick-practical-use-case-for-es6-generators-building-an-infinitely-repeating-array-49d74f555666