The behavior of the generators described in the previous article is not complex, but it is definitely surprising and may seem confusing at first. So instead of learning new concepts, we will now pause and look at an interesting example of using generators.
Let's have a function like this:
function maybeAddNumbers() {
const a = maybeGetNumberA();
const b = maybeGetNumberB();
return a + b;
}
Functions
maybeGetNumberA
and
maybeGetNumberB
return numbers, but sometimes they can return
null
or
undefined
. This is evidenced by the word "maybe" in their names. If this happens, do not try to put these values (for example, the number and
null
), it is better to stop and return, say
null
. Namely
null
, and not some unpredictable value obtained by adding
null
/
undefined
with a number or other
null
/
undefined
.
So you need to check that the numbers are actually defined:
function maybeAddNumbers() {
const a = maybeGetNumberA();
const b = maybeGetNumberB();
if (a === null || a === undefined || b === null || b === undefined) {
return null;
}
return a + b;
}
Everything works, but if it
a
is
null
or
undefined
, then there is no point in calling the function
maybeGetNumberB
. We know that it will be returned anyway
null
.
Let's rewrite the function:
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;
}
So. Instead of three simple lines of code, we quickly bloated it to 10 lines (not counting empty ones). And functions are now applied
if
, which you have to wade through to understand what the function does. And this is just an educational example! Imagine a real codebase with much more complex logic that makes such checks even more difficult. I would like to use generators here and simplify the code.
Take a look:
function* maybeAddNumbers() {
const a = yield maybeGetNumberA();
const b = yield maybeGetNumberB();
return a + b;
}
What if we could let the expression
yield <smething>
test if it is a
<smething>
real value and not
null
or
undefined
? If it turns out to be not a number, then we just stop and return
null
, as in the previous version of the code.
That is, you can write code that looks like it only works with real, defined values. The generator can check this and take appropriate action for you! Magic, right? And it's not only possible, it's easy to write!
Of course, the generators themselves do not have this functionality. They just return iterators, and you can insert values back into the generators if you want. So we need to write a wrapper, so be it
runMaybe
.
Instead of calling the function directly:
const result = maybeAddNumbers();
we will call it as a wrapper argument:
const result = runMaybe(maybeAddNumbers());
This pattern is very common in generators. By themselves, they don't know much, but with the help of self-written wrappers, you can give the generators the desired behavior! This is what we need now.
runMaybe
- a function that takes one argument: an iterator created by the generator:
function runMaybe(iterator) {
}
Let's run this iterator in a loop
while
. To do this, you need to call the iterator for the first time and start checking its property
done
:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
}
}
Inside the loop, we have two possibilities. If
result.value
is
null
or
undefined
, then the iteration should be stopped immediately and returned
null
. Let's do this:
function runMaybe(iterator) {
let result = iterator.next();
while(!result.done) {
if (result.value === null || result.value === undefined) {
return null;
}
}
}
Here, we
return
immediately stop the iteration with help and return from the wrapper
null
. But if it
result.value
is a number, then you need to "return" to the generator. For example, if the
yield maybeGetNumberA()
function
maybeGetNumberA()
is a number, then you need to replace the
yield maybeGetNumberA()
value of that number. Let me explain: let's say the result of the calculation
maybeGetNumberA()
is 5, then we replace
const a = yield maybeGetNumberA();
with
const a = 5;
. As you can see, we do not need to change the extracted value, it is enough to pass it back to the generator.
We remember that you can replace
yield <smething>
with some value by passing it as an argument to the method
next
in an iterator:
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)
}
}
As you can see, the new result is now stored in a variable again
result
. This is possible because we specifically declared
result
using
let
.
Now, if the generator detects
null
/ when retrieving a value
undefined
, we simply return
null
from the wrapper
runMaybe
.
It remains to add something else so that the iteration process ends without detecting
null
/
undefined
. After all, if we get two numbers, then we need to return their sum from the wrapper!
The generator
maybeAddNumbers
ends with an expression
return
. We understand that the presence
return <smething>
in the generator causes it to return
next
an object from the call
{ value: <smething>, done: true }
. When this happens, the loop
while
stops because the property
done
gets a value
true
. But the last value returned (in our particular case, this
a + b
) will still be stored in the property
result.value
! And we can just return it:
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;
}
And it's all!
Let's create functions
maybeGetNumberA
and
maybeGetNumberB
, and let them return real numbers first:
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => 10;
Let's run the code and log the result:
function* maybeAddNumbers() {
const a = yield maybeGetNumberA();
const b = yield maybeGetNumberB();
return a + b;
}
const result = runMaybe(maybeAddNumbers());
console.log(result);
As expected, the number 15 will appear in the console.
Now, replace one of the terms with
null
:
const maybeGetNumberA = () => null;
const maybeGetNumberB = () => 10;
When executing the code, we get
null
!
However, it is important for us to make sure that the function is
maybeGetNumberB
not called if it
maybeGetNumberA
returns
null
/
undefined
. Let's check again that the calculation was successful. To do this, just add to the second function
console.log
:
const maybeGetNumberA = () => null;
const maybeGetNumberB = () => {
console.log('B');
return 10;
}
If we have written the wrapper correctly
runMaybe
, then when this code is executed, the letter will
B
not appear in the console.
Indeed, when executing the code, we will see simply
null
. This means that the wrapper actually stops the generator as soon as it detects
null
/
undefined
.
The code works as intended: it produces
null
any combination:
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => 10;
const maybeGetNumberA = () => 5;
const maybeGetNumberB = () => null;
const maybeGetNumberA = () => undefined;
const maybeGetNumberB = () => null;
Etc.
But the benefit of this example does not lie in the execution of this particular code. It lies in the fact that we have created a universal wrapper that can work with any generator that can extract values
null
/
undefined
.
Let's write a more complex function:
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;
}
You can do it in our wrapper without any problems
runMaybe
! In fact, it doesn't even matter to the wrapper that our functions return numbers. After all, we did not mention the numeric type in it. So you can use any value in the generator - numbers, strings, objects, arrays, more complex data structures - and it will work with our wrapper!
This is what inspires developers. Generators allow you to add custom functionality to your code, which looks very common (apart from calls, of course
yield
). You just need to create a wrapper that iterates the generator in a special way. Thus, the wrapper adds the necessary functionality to the generator, which can be anything! Generators have almost limitless possibilities, it's all about our imaginations.