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

Similar Posts