ES6 Generators

O que são Generators e como eles funcionam?

Generators são funções especiais que podem ser executadas, pausadas e continuadas em diferentes estágios da sua execução, tudo isso graças a nova palavra reservada yield.

Vamos ver um exemplo:

function* myGenerator() {
  yield ‘first’;
  let input = yield ‘second’;
  yield input;
}

Para instanciar o objeto do generator:

let gen = myGenerator();

Executando o generator pela primeira vez:

console.log(gen.next());
// { value: ‘first’, done: false }

Iterando em suas etapas:

console.log(gen.next()); 
// { value: ‘second’, done: false }
// passando um valor para o próximo yield
console.log(gen.next(‘third’));
// { value: ‘third’, done: false }
console.log(gen.next()); 
// { value: undefined, done: true }

Vamos lá, o que está acontecendo aqui?

  • Nós declaramos uma função generator usando a sintaxe especial: function* myfunction() {}
  • Chamamos essa função que na sua primeira execução retorna o objeto generator. Esse objeto tem um método chamado next que executa o generator com seu estado atual.
  • O generator não é executado até a primeira chamada do .next seja executada.
  • Cada vez que .next é chamado, o generator é executado até o próximo yield. A chamada para .next retorna um objeto contendo o valor retornado pelo yield e uma flag dizendo se o generator foi finalizado ou não.
  • Você deve ter notado que podemos passar valores de volta ao generator.

Generators como iteradores

Eu disse no começo que generators eram usados como uma maneira fácil de criar iteradores. Isso é porquê ES6 permite a iteração dos valores do generator usando o operador for..of, mais o menos assim:

function* myGenerator() {
  yield ‘first’;
  yield ‘second’;
  yield ‘third’;
}
// irá imprimir 'first', 'second' e 'third'
for (var v of myGenerator()) {
  console.log(v);
}

Controle de fluxo assíncrono

Iteradores são ótimos, mas e se, generators fossem usados para controlar nosso código assíncrono?

Vamos dizer que, eu quero fazer login em algum backend e, em seguida, usar o token de resposta dessa chamada para buscar alguns dados na API. Nosso código usando Promises poderia ser mais ou menos assim:

function login(username, password) {
  return fetch(‘/login’, {
    method: ‘post’,
    body: JSON.stringify({
      username: username
      password: password
    })
  }).then(response => response.json());
}
function getPosts(token) {
  return fetch(‘/posts’, {
    headers: new Headers({
      ‘X-Security-Token’: token
    })
  }).then(response => response.json());
}
const username = ‘’;
const password = ‘’;
login(username, password)
  .then(result => getPosts(result.token))
  .then(posts => {
    console.log(posts);
  })
  .catch(err => {
    console.log(err);
  });

Porém, algumas pessoas talentosas (TJ Holowaychuk), imaginaram um cenário além, e criaram uma solução que transforma esse código em uma leitura bem mais síncrona. Ficando assim:

// usando as mesmas funções login/getPosts acima
co(function* () {
  var result = yield login(username, password);
  var posts = yield getPosts(result.token);
  return posts;
}).then(value => {
  console.log(value);
}, err => {
  console.error(err);
});

No exemplo acima, a biblioteca co, que é um runtime em torno dos generators, tornando as chamadas assíncronas parecerem mais síncronas. Para cada yield + promise, ele executa a promise e repassa o resultado para o generator. Além de promises, a biblioteca pode ser usada com thunks, arrays, objetos e tudo mais.

Indo de Express para o Koa

Em Node.js, Express tem sido o framework de escolha para serviços REST. E os criadores dele, vieram com uma nova idéia, para uma completa nova geração de possibilidades, foi então que nasceu o Koa, que usa generators intensivamente (que será substituído por async/await na próxima versão).

Continue com essa idéia de envolver generators em um runtime e aplique isso nos middlewares do express/connect. Ao invés de usarmos callbacks para chamar next, você usa irá usar yield para passar o próximo generator.

Um exemplo bem simples:

var koa = require(‘koa’);
var app = koa();
// middleware de logger
app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date — start;
  console.log(‘%s %s — %s’, this.method, this.url, ms);
});
// middleware de respostas
app.use(function *(){
  this.body = yield db.find(‘something’);
});
app.listen(3000);

Poderoso, não acha?


E no Front-End?

A mesma tendência está acontecendo no Front-End. Se você usa React/Redux e segue o que está acontecendo no ecossistema em torno deles (ou talvez tenha lido meu post anterior), você deve ter escutado sobre redux-saga.

redux-saga é um modelo para controle de side-effects em aplicações Redux. Ele toma conta do fluxo assíncrono de um modo centralizado, tudo isso usando…adivinha? 😎 Generators!

Funciona do mesmo jeito que a biblioteca co, um runtime em torno dos generators, mas quero ressaltar aqui, um idéia simples que transforma os testes em redux-saga, algo bem simples. Ao invés de executar a chamada assíncrona no próprio generator, redux-saga yield (retorna) uma descrição da chamada assíncrona que irá ser executada e delega para o runtime executar essa chamada.

Vamos escrever um pseudo-código para visualizar isso:

// usando as mesmas funções login/getPosts acima
// pseudo implementação do runtime
// apenas um exemplo para imaginação
// NÃO PERCA MUITO TEMPO AQUI
const runtime = (generator, …args) => {
  return new Promise((resolve, reject) => {
 
    let gen = generator.apply(null, args);
    const next = (ret) => {
      const { value, done } = gen.next(ret);
 
      if (done) {
        return resolve(ret);
      }
 
      // PARTE IMPORTANTE, retorna a descrição e
      // delega a execução do callback
      // lembre-se, é apenas um pseudo-código
      if (typeof value === ‘object’ && value.type === ‘call’) {
        const { context, callback, args } = value;
        return callback.apply(context, args)
          .then(res => next(res))
          .catch(err => reject(err));
      }
    // Código para outros tipos como thunks, arrays, objetos...
    };
 
    next();
  });
}
// pseudo código de efeitos do redux-saga
// NÃO PERCA MUITO TEMPO AQUI
const call = (context = null, callback, …args) => {
  return {
    type: ‘call’,
    context: context,
    callback: callback,
    args: args
  };
}
// PARTE IMPORTANTE ABAIXO
const myGenerator = function* (username, password) {
  var result = yield call(login, username, password);
  var posts = yield call(getPosts, result.token);
  return posts;
}
runtime(myGenerator, username, password)
.then(value => {
  console.log(value);
}, err => {
  console.error(err);
});

Não perca muito tempo analisando as funções runtime e call, é apenas um pseudo-código para imaginarmos o fluxo. É algo similar que acontece em co, mas com a possibilidade de executar efeitos, como o call (que geram a descrição da chamada assíncrona que irá ser executada).

Com esse pseudo-código, podemos ver que o generator que criamos não executado nenhuma das funções assíncronas dentro dele, tudo isso é delegado ao runtime.

Agora, para testar esse código, nós podemos escrever:

import { call } from 'redux-saga/effects/'
// dados para o mock
const username = ‘myUsername’;
const password = ‘myPassword’;
const myLoginResult = { token : ‘myToken’ };
const myPosts = [{ title: ‘title’ }];
// Asserts
expect(gen.next().value)
  .toEqual(call(login, username, password));
expect(gen.next(myLoginResult).value)
  .toEqual(call(getPosts, myLoginResult.token));
expect(gen.next(myPosts).value)
  .toEqual(myPosts);

Percebeu o padrão? A sequência de testes é realizada baseado no resultado de cada yield do nosso generator. E como o efeito call retorna um objeto representando a ação assíncrona, nós não precisamos de nenhum mock para o fetch.

Tornando o teste de código assíncrono, muito mais fácil e rápido!

 

Generators Javascript