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.