>

>

>

>>

>

>

>

>

>

>

>

>

>

>

>

ES6 introduziu uma nova forma de trabalhar com funções e iteradores sob a forma de Geradores (ou funções do gerador). Um gerador é uma função que pode parar a meio caminho e depois continuar de onde parou. Em resumo, um gerador parece ser uma função mas comporta-se como um iterador.

Fun Fact: async/await pode ser baseado em geradores. Leia mais aqui.

Geradores estão intrinsecamente ligados com iteradores. Se você não sabe sobre iteradores, aqui está um artigo para melhor entendimento deles.

Aqui está uma analogia simples de ter uma intuição para geradores antes de prosseguirmos com os detalhes técnicos.

Imagine que você está lendo um techno-thriller mordedor de unhas. Tudo absorvido nas páginas do livro, você mal ouve sua campainha tocar. É o entregador de pizzas. Levanta-te para abrir a porta. No entanto, antes de fazeres isso, marcas um favorito na última página que lês. Guardas mentalmente os eventos do enredo. Depois, vais buscar a tua pizza. Quando voltares ao teu quarto, começas o livro a partir da página onde colocaste o marcador de página. Você não o começa da primeira página de novo. De certa forma, você agiu como uma função geradora.

Vejamos como podemos utilizar geradores para resolver alguns problemas comuns enquanto programamos. Mas antes disso, vamos definir o que são geradores.

O que são geradores?

Uma função normal como esta não pode ser parada antes de terminar sua tarefa, ou seja, sua última linha é executada. Segue algo chamado run-to-completion model.

function normalFunc() {
console.log('I')
console.log('cannot')
console.log('be')
console.log('stopped.')
}

A única maneira de sair do modelo normalFunc é por returning dele, ou por throwing um erro. Se você chamar a função novamente, ela começará a execução do topo novamente.

Em contraste, um gerador é uma função que pode parar a meio caminho e depois continuar de onde parou.

Aqui estão algumas outras definições comuns de geradores –

  • Geradores são uma classe especial de funções que simplificam a tarefa de escrever iteradores.
  • Um gerador é uma função que produz uma sequência de resultados em vez de um único valor, ou seja, você gera uma série de valores.

Em JavaScript, um gerador é uma função que retorna um objeto sobre o qual você pode chamar next(). Cada invocação de next() irá retornar um objeto de forma –

{ 
value: Any,
done: true|false
}

A propriedade value irá conter o valor. A propriedade done é ou true ou false. Quando a done se torna true, o gerador pára e não gera mais nenhum valor.

Aqui está uma ilustração do mesmo –

>

>

>

>

>

Funções Normais vs Geradores

Note a seta a tracejado que fecha o laço de campo-resumo-rendimento pouco antes de Terminar em Geradores parte da imagem. Existe a possibilidade de que um gerador nunca termine. Veremos um exemplo mais tarde.

Criar um Gerador

Vejamos como podemos criar um gerador em JavaScript –

function * generatorFunction() { // Line 1
console.log('This will be executed first.');
yield 'Hello, '; // Line 2 console.log('I will be printed after the pause');
yield 'World!';
}const generatorObject = generatorFunction(); // Line 3console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined

Focalizar nas partes em negrito. Para criar uma função de gerador, usamos function * sintaxe em vez de apenas function. Qualquer número de espaços pode existir entre a palavra-chave function, a *, e o nome da função. Como é apenas uma função, você pode usá-la em qualquer lugar que uma função possa ser usada, ou seja, dentro de objetos, e métodos de classe.

Dentro do corpo da função, nós não temos um return. Ao invés disso, temos outra palavra-chave yield (Linha 2). É um operador com o qual um gerador pode fazer uma pausa a si mesmo. Cada vez que um gerador encontra um yield, ele “retorna” o valor especificado após ele. Neste caso, Hello, é retornado. No entanto, não dizemos “retornado” no contexto dos geradores. Dizemos que o “gerador rendeu Hello, “.

Também podemos retornar de um gerador. No entanto, return define a propriedade done para true após o que o gerador não pode gerar mais valores.

function * generatorFunc() {
yield 'a';
return 'b'; // Generator ends here.
yield 'a'; // Will never be executed.
}

Na linha 3, criamos o objeto gerador. Parece que estamos a invocar a função generatorFunction. De fato, estamos! A diferença é que ao invés de retornar qualquer valor, uma função geradora sempre retorna um objeto gerador. O objeto gerador é um iterador. Então você pode usá-lo em for-of loops ou outras funções aceitando um iterável.

Na linha 4, chamamos o método next() no método generatorObject. Com esta chamada, o gerador começa a ser executado. Primeiro, ele console.log o This will be executed first. Depois, ele encontra um yield 'Hello, '. O gerador produz o valor como um objeto { value: 'Hello, ', done: false } e suspende/pausa. Agora, ele está esperando pela próxima invocação.

Na linha 5, chamamos de novo next(). Desta vez o gerador acorda e começa a executar de onde ele saiu. A próxima linha que ele encontra é um console.log. Ele registra a string I will be printed after the pause. Outra yield é encontrada. O valor é renderizado como o objeto { value: 'World!', done: false }. Extraímos a propriedade value e registramo-la. O gerador dorme novamente.

Na linha 6, invocamos novamente next(). Desta vez não há mais linhas para executar. Lembre-se que cada função retorna implicitamente undefined se nenhuma declaração de retorno for fornecida. Portanto, o gerador retorna (ao invés de render) um objeto { value: undefined, done: true}. O done é definido como true. Isto sinaliza o fim deste gerador. Agora, ele não pode gerar mais valores ou retomar novamente já que não há mais comandos a serem executados.

Temos que fazer um novo objeto gerador para executar o gerador novamente.

Uses of Generators

Existem muitos casos incríveis de uso de geradores. Vamos ver alguns deles.

Implementando Iterables

Quando você implementa um iterator, você tem que fazer manualmente um objeto iterator com um método next(). Além disso, você tem que salvar o estado manualmente. Muitas vezes, torna-se realmente difícil fazer isso. Como os geradores também são iterables, eles podem ser usados para implementar iterables sem o código extra da placa de caldeira. Vamos ver um exemplo simples.

Problem: Queremos fazer um iterável personalizado que retorna This, is, e iterable.. Aqui está uma implementação usando iteradores –

const iterableObj = {
() {
let step = 0;
return {
next() {
step++;
if (step === 1) {
return { value: 'This', done: false};
} else if (step === 2) {
return { value: 'is', done: false};
} else if (step === 3) {
return { value: 'iterable.', done: false};
}
return { value: '', done: true };
}
}
},
}for (const val of iterableObj) {
console.log(val);
}// This
// is
// iterable.

Aqui está a mesma coisa usando geradores –

function * iterableObj() {
yield 'This';
yield 'is';
yield 'iterable.'
}for (const val of iterableObj()) {
console.log(val);
}// This
// is
// iterable.

Pode comparar as duas versões. É verdade que isto é um exemplo. Mas ilustra os pontos –

  • Não temos de nos preocupar com Symbol.iterator
  • Não temos de implementar next().
  • Não temos de fazer manualmente o objecto de retorno de next() i.e. { value: 'This', done: false }.
  • Não temos de salvar o estado. No exemplo do iterador, o estado foi salvo na variável step. É o valor definido o que saiu do iterável. Não tivemos que fazer nada desse tipo no gerador.

Melhor funcionalidade Async

Código usando promessas e callbacks como –

function fetchJson(url) {
return fetch(url)
.then(request => request.text())
.then(text => {
return JSON.parse(text);
})
.catch(error => {
console.log(`ERROR: ${error.stack}`);
});
}

pode ser escrito como (com a ajuda de bibliotecas como co.js)-

const fetchJson = co.wrap(function * (url) {
try {
let request = yield fetch(url);
let text = yield request.text();
return JSON.parse(text);
}
catch (error) {
console.log(`ERROR: ${error.stack}`);
}
});

Alguns leitores devem ter notado que ele é paralelo ao uso de async/await. Isso não é uma co-incidência. async/await pode seguir uma estratégia semelhante e substitui o rendimento por await nos casos em que as promessas estão envolvidas. Pode ser baseado em geradores. Veja este comentário para mais informações.

Fluxos de Dados Infinitos

É possível criar geradores que nunca terminam. Considere este exemplo –

function * naturalNumbers() {
let num = 1;
while (true) {
yield num;
num = num + 1
}
}const numbers = naturalNumbers();console.log(numbers.next().value)
console.log(numbers.next().value)// 1
// 2

Fazemos um gerador naturalNumbers. Dentro da função, nós temos um infinito while loop. Nesse laço, nós yield o num. Quando o gerador cede, ele é suspenso. Quando chamamos novamente next(), o gerador acorda, continua de onde foi suspenso (neste caso yield num) e executa até que outro yield seja encontrado ou o gerador termina. Como a próxima afirmação é num = num + 1, ele atualiza num. Então, ele vai para o topo do loop while. A condição ainda é verdadeira. Ele encontra a próxima linha yield num. Ele produz a atualização num e suspende. Isto continua enquanto você quiser.

Geradores como observadores

Geradores também podem receber valores usando a função next(val). Então o gerador é chamado de observador uma vez que acorda quando recebe novos valores. Em certo sentido, ele continua observando por valores e age quando recebe um. Você pode ler mais sobre este padrão aqui.

Vantagens dos Geradores

Como visto com o exemplo Infinite Data Streams, só é possível por causa da avaliação preguiçosa. Avaliação preguiçosa é um modelo de avaliação que atrasa a avaliação de uma expressão até que seu valor seja necessário. Ou seja, se não precisarmos do valor, ele não existirá. Ela é calculada como nós a exigimos. Vejamos um exemplo –

function * powerSeries(number, power) {
let base = number;
while(true) {
yield Math.pow(base, power);
base++;
}
}

O powerSeries dá a série do número elevado a um poder. Por exemplo, a série de potências de 3 elevado a 2 seria 9(3²) 16(4²) 25(5²) 36(6²) 49(7²). Quando fazemos const powersOf2 = powerSeries(3, 2); apenas criamos o objecto gerador. Nenhum dos valores foi computado. Agora, se chamarmos next(), 9 seria computado e retunido.

Memory Efficient

Uma consequência direta da Lazy Evaluation é que os geradores são eficientes em termos de memória. Geramos apenas os valores que são necessários. Com funções normais, precisávamos pré-gerar todos os valores e mantê-los em torno, caso os utilizemos mais tarde. No entanto, com geradores, podemos adiar o cálculo até precisarmos dele.

Podemos criar funções combinadas para atuar em geradores. Combinadores são funções que combinam iterables existentes para criar novos. Um desses combinadores é take. É necessário primeiro n elementos de um iterável. Aqui está uma implementação –

function * take(n, iter) {
let index = 0;
for (const val of iter) {
if (index >= n) {
return;
}
index = index + 1;
yield val;
}
}

Aqui estão alguns casos interessantes de uso de take

take(3, )// a b ctake(7, naturalNumbers());// 1 2 3 4 5 6 7take(5, powerSeries(3, 2));// 9 16 25 36 49

Aqui está uma implementação de biblioteca cíclica (sem a funcionalidade de inversão).

function * cycled(iter) {
const arrOfValues =
while (true) {
for (const val of arrOfValues) {
yield val
}
}
}console.log(...take(10, cycled(take(3, naturalNumbers()))))// 1 2 3 1 2 3 1 2 3 1

Cavernas

Existem alguns pontos que você deve lembrar enquanto programar usando geradores.

  • Os objetos do gerador são de acesso único. Uma vez que você tenha esgotado todos os valores, você não pode iterar sobre ele novamente. Para gerar os valores novamente, você precisa fazer um novo objeto gerador.
const numbers = naturalNumbers();console.log(...take(10, numbers)) // 1 2 3 4 5 6 7 8 9 10
console.log(...take(10, numbers)) // This will not give any data
  • Objetos geradores não permitem o acesso aleatório possível com arrays. Como os valores são gerados um a um, acessar um valor aleatório levaria ao cálculo de valores até aquele elemento. Portanto, não é acesso aleatório.

Conclusão

Muitas coisas ainda não foram cobertas pelos geradores. Coisas tais como yield *, return() e throw(). Os geradores também tornam possíveis as coroutinas. Eu listei algumas referências que você pode ler para entender melhor os geradores.

Você pode ir para a página de iteradores Python, e ver algumas das utilidades que permitem trabalhar com iteradores e geradores. Como um exercício, você mesmo pode implementar as utilidades.

admin

Deixe uma resposta

O seu endereço de email não será publicado.

lg