Funções Geradoras e Geradoras Assíncronas em Javascript
Vou explicar e mostrar como usar funções geradoras e geradoras assíncronas no javascript. Mas, eu preciso explicar outras funcionalidades antes.
Iteradores
Um iterador (iterator) é um objeto que sabe como acessar uma coleção de dados, um por vez.
Criando um Iterador
Para criar um iterador você precisa que o objeto siga o protocolo iterator
, ou seja, ele precisa ter um método next
. Esse método deve retorna um objeto com as propriedades value
, que terá o valor a ser acessado, e done
que irá indicar se os dados acabaram.
const makeIterator = (array) => {
let nextIndex = 0;
const iterator = {
// Implementado protocolo iterator
next() {
if (nextIndex < array.length) {
return {
value: array[nextIndex++],
done: false
}
}
return { done: true }
}
}
return iterator;
}
A função makeIterator
irá criar receber um array
e irá retornar um iterador, objeto com o método next
, para acessar os dados desse array
.
Os dados podem ser acessados chamando o método next
. Quando os dados acabarem você receberá um objeto com { done: true }
const array = [1, 2, 3, 4, 5];
const iterator = makeIterator(array);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { done: true }
// Não há mais valores para iterar
Um iterador pode ser acessado mais facilmente através de laços de repetição como while
, do...while
, for
.
{
const array = [1, 2, 3, 4, 5];
const iterator = makeIterator(array);
console.log("while");
let result = iterator.next();
while (result.done === false) {
console.log(result);
result = iterator.next();
}
}
{
const array = [1, 2, 3, 4, 5];
const iterator = makeIterator(array);
console.log("do while")
let result = iterator.next();
do {
console.log(result);
result = iterator.next();
} while (result.done === false);
}
{
const array = [1, 2, 3, 4, 5];
const iterator = makeIterator(array);
console.log("for")
for (let result = iterator.next(); result.done === false; result = iterator.next()) {
console.log(result);
}
}
Iterador assíncrono
Para dados assíncronos existe o iterador assíncrono que também possui o método next
no seu protocolo, mas agora retorna uma Promise
const makeAsyncIterator = (array) => {
let nextIndex = 0;
const iterator = {
// Ao criar um função com async ela já retorna um Promise por padrão
async next() {
if (nextIndex < array.length) {
return {
value: array[nextIndex++],
done: false
}
}
return { done: true }
}
}
return iterator;
}
const array = [1, 2, 3, 4];
const asyncIterator = makeAsyncIterator(array);
const run = async () => {
console.log(await asyncIterator.next()); // { value: 1, done: false }
console.log(await asyncIterator.next()); // { value: 2, done: false }
console.log(await asyncIterator.next()); // { value: 3, done: false }
console.log(await asyncIterator.next()); // { value: 4, done: false }
console.log(await asyncIterator.next()); // { done: true }
}
run()
Iteráveis
Um objeto é iterável (iterable) se ele tem o comportamento de iteração. objetos como Array
, Map
, TypedArray
, Set
, String
, e aqueles que podem ser acessados com o loop for...of
são iteráveis.
Criando um Iterável
Para criar um iterável o objeto precisa implementar o protocolo iterable
. Para isso é preciso criar o método [Symbol.iterator]()
que retorna um iterador (iterator).
Vamos modificar a nossa função makeIterator
para o objeto itarador ser um iteravel também.
const makeIterator = (array) => {
let nextIndex = 0;
const iterator = {
next() {
if (nextIndex < array.length) {
return {
value: array[nextIndex++],
done: false
}
}
return { done: true }
},
// Implementando o protocolo iterable
[Symbol.iterator]() {
// precisamos retornar um iterator
// então vamos retornar o próprio objeto
return this;
}
}
return iterator;
}
Agora nossa função retorna um objeto que é um iterador, implementa o protocolo iterator
com o método next()
, e um iteravel, implementa o protocolo iterable
com o método [Symbol.iterator]()
.
Com um iteravel podemos percorrer ele em um loop for...of
. Ou mesmo espalhar seus valores usando o spread operator (...
).
{
const array = ["Ana", "Matheus", "Gabriel", "Larissa", "João"];
const iterator = makeIterator(array);
console.log("for of")
for (const value of iterator) {
console.log(value);
// Matheus
// Ana
// Gabriel
// Larissa
// João
}
}
{
const array = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const iterator = makeIterator(array);
console.log("spread operator")
console.log([...iterator]);
// [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 } ]
}
Iterável assíncrono
Para dados assíncronos também temos o iterável assíncrono que segue o protocolo async iterable
através do método [Symbol.asyncIterable]()
que retorna um iterador assíncrono.
const makeAsyncIterator = (array) => {
let nextIndex = 0;
const iterator = {
async next() {
if (nextIndex < array.length) {
return {
value: array[nextIndex++],
done: false
}
}
return { done: true }
},
// Implementando o protocolo async iterable
[Symbol.asyncIterator]() {
// precisamos retornar um async iterator
// então vamos retornar o próprio objeto
return this;
}
}
return iterator;
}
const array = [1, 2, 3, 4];
const asyncIterator = makeAsyncIterator(array);
const run = async () => {
// Para iteráveis assíncronos usamos o for await...of
for await (const value of asyncIterator) {
console.log(value);
// 1
// 2
// 3
// 4
}
};
run();
Função Geradora
Funções geradoras são um tipo especial de função. Elas retornar um objeto Generator
que uma subclasse de Iterator
. Basicamente a função retorna um objeto iterador que também implementa o protocolo iteravel. É uma forma mais fácil de criar um objeto itarador/iteravel. Ou seja, o objeto da classe Generator
possui os métodos next()
e [Symbom.iterator]()
.
Para criar uma função geradora é simples, basta usar a sintaxe function*
e retornar os valores usando a palavra yield
.
function* makeGenerator() {
let value = 1;
while (true) {
// retorna o valor atual e incrementa 1
yield value++;
if (value >= 5) {
// quando o valor atingir 5, a função geradora termina
return;
}
}
}
Agora podemos criar um objeto Generator
e usa-lo como iterador/iterável.
{
const generator = makeGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: 4, done: false }
console.log(generator.next()); // { value: undefined, done: true }
}
{
const generator = makeGenerator();
const data = [...generator];
console.log(data); // [ 1, 2, 3, 4 ]
}
{
const generator = makeGenerator();
for (const value of generator) {
console.log(value);
// 1
// 2
// 3
// 4
}
}
Exemplo de uso
Com a função geradora podemos gerar dados sobre demanda. Um uso simples da função geradora para criar a função de fibonacci.
function* fibonacci() {
let a = 0;
let b = 1;
while (true) { // Sequência infinita
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
console.log(fib.next().value); // 2
console.log(fib.next().value); // 3
// ... e assim por diante, gerando valores apenas quando solicitados
Função Geradora Assíncrona
Se precisarmos trabalhar com dados assíncronos sob demanda, podemos usar a função geradora assíncrona. Assim como a função geradora sincrona, a assíncrona retorna valores sob demanda através do yield
, só que dessa vez ela retorna uma Promise
.
async function* makeAsyncGenerator() {
let value = 1;
while (true) {
yield value++;
if (value >= 5) {
return;
}
}
}
E assim para consumir os dados é só chamar a função next
que ela irá retorna uma Promise
com o valor.
const run = async () => {
const asyncGenerator = makeAsyncGenerator();
console.log(await asyncGenerator.next()); // { value: 1, done: false }
console.log(await asyncGenerator.next()); // { value: 2, done: false }
console.log(await asyncGenerator.next()); // { value: 3, done: false }
console.log(await asyncGenerator.next()); // { value: 4, done: false }
console.log(await asyncGenerator.next()); // { value: undefined, done: true }
}
run();
Exemplo de uso
Um caso de uso interessante de geradores assíncronos é na manipulação de streams de dados no NodeJS. Onde você pode criar um gerador para transformar os dados dentro de um pipeline.
nomes.txt
Ana Silva
Carlos Oliveira
Mariana Santos
Pedro Almeida
Juliana Costa
Rafael Pereira
Camila Rodrigues
Bruno Fernandes
Larissa Martins
Diego Nascimento
Beatriz Lima
Gustavo Barbosa
Isabela Carvalho
Leonardo Souza
Fernanda Ribeiro
Thiago Cardoso
Natália Gomes
Alexandre Torres
Priscila
index.mjs
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
async function* asyncGenerator(stream) {
// consumindo dados da stream
for await (const chunk of stream) {
const name = chunk.toString();
// transformando o nome para maiúsculas
const nameUpperCase = name.toUpperCase();
// enviando o nome transformado para a próxima etapa do pipeline sob demanda
yield nameUpperCase;
}
}
const run = async () => {
// Cria uma stream para ler os dados do arquivo nomes.txt
const stream = createReadStream("nomes.txt", { encoding: "utf-8" });
/**
* pipeline é uma função que conecta uma série de streams
* os dados são lidos da variavel stream
* e mandando para a função geradora asyncGenerator
* que transforma os dados e envia para a saída padrão (process.stdout)
*/
await pipeline(
stream,
asyncGenerator,
process.stdout
);
};
run();
saída
> node index.mjs
ANA SILVA
CARLOS OLIVEIRA
MARIANA SANTOS
PEDRO ALMEIDA
JULIANA COSTA
RAFAEL PEREIRA
CAMILA RODRIGUES
BRUNO FERNANDES
LARISSA MARTINS
DIEGO NASCIMENTO
BEATRIZ LIMA
GUSTAVO BARBOSA
ISABELA CARVALHO
LEONARDO SOUZA
FERNANDA RIBEIRO
THIAGO CARDOSO
NATÁLIA GOMES
ALEXANDRE TORRES
PRISCILA