(JavaScript ES6) Generators [PT-BR]

MAGIA NEGRA!

Magia negra

Para quem está por dentro sobre NodeJS, um assunto que está bombando é tratar instruções assíncronas como se fosse instruções síncronas utilizando Generators. Coroutines.

TL;DR; Generator é uma função pausável.

A lib co faz uma implementação com generators que permite codificar código assíncrono como se fossem síncrono! O Bluebird também tem uma implementação de coroutine.

Antes de continuar, eu vou assumir que você manja de Promises. Se você ainda não sabe nada de Promises esse post vai explodir sua cabeça. Recomendo que dê uma olhada no vídeo desse cara: https://www.youtube.com/watch?v=2d7s3spWAzo


Vamos para um exemplo de implementação.

Primeiro, utilizando Promise.

Vou usar a lib node-fetch que implementa o FetchAPI para Node.

$ npm i node-fetch 

Criei um arquivo chamado sem-bacon.js.

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
fetch(uri)
.then((response) => response.text())
.then((result) => console.log('result:', result));

Vamos rodar isso:

$ node sem-bacon.js
* Vai aparecer algo assim:
result: "Darien Zulauf lives on Madagascar"

Legal, agora utilizando Generators:

Primeiro passo é instalar a lib co:

$ npm i co

Criar um arquivo… sei lá, vamos chamar de co-bacon.js:

const fetch = require('node-fetch');
const co = require('co');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
const response = yield fetch(uri);
const result = yield response.text();

console.log('result': result);
});

Ok, adivinha o que aquele console.log vai mostrar?

$ node co-bacon.js
* Vai aparecer algo assim:
result: "Myah lives on Lao People's Democratic Republic"

Beleza, você deve estar pensando: “espera, o fetch não retorna uma promise? cadê o .then”, “o que é a estrelinha depois da function?!”, “QUE P*RRA É ESSE yield?!!”.

Muito bem, isso se chama magia negra.

WTF CHEESUS

Brincadeiras à parte, vamos começar pela function* essa function tem um nome: Generator Function. Uma função especial que cria um generator.

Pense que Generators são funções que podem ser pausadas.

Quando a função está sendo executada, ao chegar na instrução yield sua execução é pausada e a tarefa de continuar é delegada para quem está chamando o generator.

Complicado? Hm. Melhor partirmos para o exemplo, ficará mais claro assim.

Vamos tentar implementar a mesma coisa que a lib co está fazendo.

Primeiro, vamos criar uma function chamada co, que recebe um generator e, por enquanto, vamos pedir só para ele executar o generator. Só para ver se o generator está sendo executado, vou colocar um console.log logo no início dela.

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
console.log('Executando a generator function!');
const response = yield fetch(uri);
const result = yield response.text();

console.log('result:', result);
});
function co(generator) {
generator();
}

Ao executar:

$ node co-bacon.js

Nada! Nada acontece. A generator não foi chamado! Droga!

Acalme-se. Ela foi “executada”, fiz isso de propósito.

O que acontece, na verdade, ao executar um generator ele não vai executar a função logo de cara. Ele te retorna um iterator.

Ao chamar um método chamado next desse iterator, a função é executada até atingir um o primeiro yield. Vamos tentar:

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
console.log('Executando a generator function!');
const response = yield fetch(uri);
const result = yield response.text();

console.log('result:', result);
});
function co(generator) {
const iterator = generator();
const iteration = iterator.next();

console.log('iteration:', iteration);
}

Executando…

$ node co-bacon.js
Executando a generator function!
iteration: { value: Promise { <pending> }, done: false }

Ok. Agora sim o generator foi executado. Ele retornou um novo objeto. Dessa vez ele contém duas propriedades: value e done.

Por hora, vamos focar no value. Como vocês podem imaginar, ele contém o valor que é passado para o yield, dentro da generator function, ou seja, a nossa promise do fetch.

Beleza, para dar o próximo devemos chamar novamente o next do iterator. Mas, antes de dar o próximo passo, vamos resolver a nossa promise.

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
const response = yield fetch(uri);
console.log('Chegamos no segundo yield!');
const result = yield response.text();

console.log('result:', result);
});
function co(generator) {
const iterator = generator();
const iteration = iterator.next();
const promise = iteration.value;

promise.then((response) => {
const secondIteration = iterator.next(response);

console.log('secondIteration', secondIteration);
});

}

Ao executar:

$ node co-bacon.js
Chegamos no segundo yield!
secondIteration: { value: Promise { <pending> }, done: false }

Sim, retornou uma promise, novamente, mas dessa vez é a promise da execução do yield response.text().

O que aconteceu: pegamos a promise que estava dentro de iteration.value, usamos o then e passamos o resultado como argumento do próximo iterator.next(response), assim o response retornará para o yield.

A execução do iterator.next(result) vai retomar a execução, e, pausará no próximo yield.

O iterator retornou, então, a nossa segunda iteração com uma nova promise, dessa vez a promise da execução do response.text().

Faremos a mesma coisa, executar a promise e chamar o próximo iterator.next:

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
const response = yield fetch(uri);
const result = yield response.text();

console.log('result:', result);
});
function co(generator) {
const iterator = generator();
const iteration = iterator.next();
const promise = iteration.value;

promise.then((response) => {
const secondIteration = iterator.next(response);
const secondPromise = secondIteration.value;

secondPromise.then((result) => {
const lastIteration = iterator.next(result);

console.log('lastIteration:', lastIteration);
});

});
}

Ao executar:

$ node co-bacon.js
result: Eino lives on Saint Lucia
lastIteration: { value: undefined, done: true }

Opa, Conseguimos!

É bom observar que a propriedade done está como true dessa vez, já dá pra imaginar o que ele diz, certo?

É isso! Terminamos! Conseguimos implementar código assíncrono como se fosse síncrono! Nosso trabalho está feito!


Bom, mais ou menos, certo?

Olha para código, além dele só conseguir executar até o segundo yield, e não podemos extender esse promise hell até o infinito.

Bom, vamos dar uma melhorada nele, e implementar ele com um pouco de recursão:

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
const response = yield fetch(uri);
const result = yield response.text();

console.log('result:', result);
});
function co(generator) {
const iterator = generator();
  function iterate(iteration) {
if (iteration.done) {
return iteration.value;
}

const promise = iteration.value;

promise.then((result) => {
const nextIteration = iterator.next(result);
iterate(nextIteration)
});
}

iterate(iterator.next());
}

Bom. É isso. Agora você é tão badass quanto esse gato.


Apesar de Generator ser uma feature muito legal, que encaixa muito bem em implementações de código assíncrono, não podemos deixar de lado o poder das Promises.

Temos que ver se realmente é necessário utilizar Generators. Nesse caso eu, particularmente, gosto mais da implementação com Promises do que com Generators.

Promise:

const fetch = require('node-fetch');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
fetch(uri)
.then((response) => response.text())
.then((result) => console.log('result:', result));

Generator:

const fetch = require('node-fetch');
const co = require('co');
const uri = 'https://fakemytemplate.herokuapp.com/?q={{name.firstName}} lives on {{address.country}}';
co(function* () {
const response = yield fetch(uri);
const result = yield response.text();

console.log('result:', result);
});

Mas casos são casos, este é apenas um exemplo, tem lugar certo para implementar Promises e Generators.

A lib koa.js utiliza os generators de uma maneira muito legal para fazer implementações que poderiam ser bem custosas em comparação ao tradicional express.

Um post legal é o do tj, criador da lib co (e de muitas outras por aí).

É bom também dar uma olhada no conteúdo da documentação na MDN.

Esse post foi inspirado pelo vídeo do canal Funfunfunction, vale a pena dar uma olhada nos vídeos dele. Considero um dos melhores canais para quem quer aprender mais sobre Javascript.