O Guia Avançado de JavaScript: Geradores. Parte 2, um caso de uso simples



O comportamento dos geradores descritos no artigo anterior não é complexo, mas é definitivamente surpreendente e pode parecer confuso no início. Portanto, em vez de aprender novos conceitos, faremos uma pausa e examinaremos um exemplo interessante de uso de geradores.



Vamos ter uma função como esta:



function maybeAddNumbers() {
    const a = maybeGetNumberA();
    const b = maybeGetNumberB();

    return a + b;
}

      
      





Funções maybeGetNumberA



e maybeGetNumberB



números de retorno, mas às vezes eles podem retornar null



ou undefined



. Isso é evidenciado pela palavra "talvez" em seus nomes. Se isso acontecer, não tente colocar esses valores (por exemplo, o número e null



), é melhor parar e voltar, digamos null



. Ou seja null



, e não algum valor imprevisível obtido pela adição de null



/ undefined



com um número ou outro null



/ undefined



.



Portanto, você precisa verificar se os números estão realmente definidos:



function maybeAddNumbers() {
    const a = maybeGetNumberA();
    const b = maybeGetNumberB();

    if (a === null || a === undefined || b === null || b === undefined) {
        return null;
    }

    return a + b;
}

      
      





Tudo funciona, mas se a



for null



ou undefined



, então não há porque chamar a função maybeGetNumberB



. Sabemos que ele será devolvido de qualquer maneira null



.



Vamos reescrever a função:



function maybeAddNumbers() {
    const a = maybeGetNumberA();

    if (a === null || a === undefined) {
        return null;
    }

    const b = maybeGetNumberB();

    if (b === null || b === undefined) {
        return null;
    }

    return a + b;
}

      
      





Assim. Em vez de três linhas simples de código, rapidamente aumentamos para 10 linhas (sem contar as vazias). E as funções agora são aplicadas if



, as quais você deve percorrer para entender o que a função faz. E este é apenas um exemplo educacional! Imagine uma base de código real com uma lógica muito mais complexa, tornando essas verificações ainda mais difíceis. Eu gostaria de usar geradores aqui e simplificar o código.



Dê uma olhada:



function* maybeAddNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();

    return a + b;
}

      
      





E se pudéssemos deixar a expressão yield <smething>



testar se é um <smething>



valor real e não null



ou undefined



? Se não for um número, então paramos e voltamos null



, como na versão anterior do código.



Ou seja, você pode escrever código que olha como ele só funciona com valores reais, definidos. O gerador pode verificar isso e tomar as medidas adequadas para você! Magia, certo? E não só é possível, como também é fácil de escrever!



Obviamente, os próprios geradores não possuem essa funcionalidade. Eles apenas retornam iteradores e você pode inserir valores de volta nos geradores, se desejar. Portanto, precisamos escrever um wrapper, que assim seja runMaybe



.



Em vez de chamar a função diretamente:



const result = maybeAddNumbers();

      
      





vamos chamá-lo de um argumento wrapper:



const result = runMaybe(maybeAddNumbers());

      
      





Esse padrão é muito comum em geradores. Por si próprios, eles não sabem muito, mas com a ajuda de invólucros escritos por eles mesmos, você pode dar aos geradores o comportamento desejado! É disso que precisamos agora.



runMaybe



- uma função que leva um argumento: um iterador criado pelo gerador:



function runMaybe(iterator) {

}

      
      





Vamos executar este iterador em um loop while



. Para fazer isso, você precisa chamar o iterador pela primeira vez e começar a verificar sua propriedade done



:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {

    }
}

      
      





Dentro do loop, temos duas possibilidades. Se result.value



for null



ou undefined



, a iteração deve ser interrompida imediatamente e retornada null



. Vamos fazer isso:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }
    }
}

      
      





Aqui, return



paramos imediatamente a iteração com ajuda e retornamos do wrapper null



. Mas se result.value



for um número, você precisa "retornar" ao gerador. Por exemplo, se a yield maybeGetNumberA()



função maybeGetNumberA()



for um número, você precisará substituir o yield maybeGetNumberA()



valor desse número. Deixe-me explicar: digamos que o resultado do cálculo maybeGetNumberA()



seja 5, então substituímos const a = yield maybeGetNumberA();



por const a = 5;



. Como você pode ver, não precisamos alterar o valor extraído, basta passá-lo de volta para o gerador.



Lembramos que você pode substituir yield <smething>



por algum valor, passando-o como um argumento para o método next



em um iterador:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }

        // we are passing result.value back
        // to the generator
        result = iterator.next(result.value)
    }
}

      
      





Como você pode ver, o novo resultado agora está armazenado em uma variável novamente result



. Isso é possível porque declaramos especificamente result



usar let



.



Agora, se o gerador encontrar um null



/ ao recuperar um valor undefined



, simplesmente retornamos null



do invólucro runMaybe



.



Resta adicionar algo mais para que o processo de iteração termine sem detectar null



/ undefined



. Afinal, se obtivermos dois números, precisaremos retornar a soma deles da embalagem!



O gerador maybeAddNumbers



termina com uma expressão return



. Nós entendemos que a presença return <smething>



no gerador faz com que ele retorne next



um objeto da chamada { value: <smething>, done: true }



. Quando isso acontece, o loop while



para porque a propriedade done



obtém um valor true



. Mas o último valor retornado (em nosso caso particular, este a + b



) ainda será armazenado na propriedade result.value



! E podemos apenas devolvê-lo:



function runMaybe(iterator) {
    let result = iterator.next();

    while(!result.done) {
        if (result.value === null || result.value === undefined) {
            return null;
        }

        result = iterator.next(result.value)
    }

    // just return the last value
    // after the iterator is done
    return result.value;
}

      
      





E é tudo!



Vamos criar funções maybeGetNumberA



e maybeGetNumberB



deixar que retornem números reais primeiro:



const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => 10;

      
      





Vamos executar o código e registrar o resultado:



function* maybeAddNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();

    return a + b;
}

const result = runMaybe(maybeAddNumbers());

console.log(result);

      
      





Como esperado, o número 15 aparecerá no console.



Agora, substitua um dos termos por null



:



const maybeGetNumberA = () => null;
const maybeGetNumberB = () => 10;

      
      





Ao executar o código, obtemos null



!



No entanto, é importante para nós garantir que a função maybeGetNumberB



não seja chamada se maybeGetNumberA



retornar null



/ undefined



. Vamos verificar novamente se o cálculo foi bem-sucedido. Para fazer isso, basta adicionar à segunda função console.log



:



const maybeGetNumberA = () => null;
const maybeGetNumberB = () => {
    console.log('B');
    return 10;
}

      
      





Se tivermos escrito o wrapper corretamente runMaybe



, quando este código for executado, a letra B



não aparecerá no console.



Na verdade, ao executar o código, veremos de forma simples null



. Isso significa que o wrapper realmente pára o gerador assim que detecta null



/ undefined



.



O código funciona como pretendido: ele produz null



qualquer combinação:



const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => 10;
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => null;
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => null;

      
      





Etc.



Mas o benefício deste exemplo não está na execução deste código específico. Está no fato de que criamos um wrapper universal que pode funcionar com qualquer gerador que extraia valores null



/ undefined



.



Vamos escrever uma função mais complexa:



function* maybeAddFiveNumbers() {
    const a = yield maybeGetNumberA();
    const b = yield maybeGetNumberB();
    const c = yield maybeGetNumberC();
    const d = yield maybeGetNumberD();
    const e = yield maybeGetNumberE();
    
    return a + b + c + d + e;
}

      
      





Você pode fazer isso em nossa embalagem sem problemas runMaybe



! Na verdade, nem mesmo importa para o wrapper que nossas funções retornem números. Afinal, não mencionamos o tipo numérico nele. Portanto, você pode usar qualquer valor no gerador - números, strings, objetos, arrays, estruturas de dados mais complexas - e funcionará com nosso wrapper!



É isso que inspira os desenvolvedores. Os geradores permitem que você adicione funcionalidade personalizada ao seu código, o que parece muito comum (além das chamadas, é claro yield



). Você só precisa criar um wrapper que itere o gerador de uma maneira especial. Assim, o wrapper adiciona a funcionalidade necessária ao gerador, que pode ser qualquer coisa! Os geradores têm possibilidades quase ilimitadas, é tudo sobre a nossa imaginação.



All Articles