As we saw in the post “Make your ES6 classes iterable“, you can make a class iterable by returning a specific object containing a “next” function used by the “for … of” loop to get the different values. As a reminder, the “next” function returns an object with two properties:
- “value” defines the current value of the loop.
- “done” defines whether the loop is completed or not.
Even though this is pretty nice, the resulting code is a bit big. Indeed, the final code of the aforementioned posted looked like:
Besides the length of the code, we were forced to maintain an internal state to know which object to return next (the “_index” variable). It is possible to simplify all of this by using “generator functions” that are functions able to maintain an internal state. I could throw two more complicated sentences explaining what generators are but I don’t think that it would help, so let’s use some examples to understand this.
Yield is a star
Behind this fancy title is hiding the core concept of generator functions. Declaring such a function is done by using a star:
The star (“*”) defines that the function is a generator, meaning that it returns a “Generator” object that can be used to get the next value of the loop and… Wait, what? What loop, what value?
Okay, let’s put a bit of context here. As we said, generator functions are different from normal ones. When calling such a function, its body won’t be executed right away, indeed, it will be executed when the “next” function of the returned “Generator” object gets called. This execution will then pause once the “yield” keyword is reached allowing it to be resumed later.
For instance, imagine that you want to create a generator function to get random numbers. This could be done like this:
Wow ! What do I see here? An infinite loop?! Indeed, there is an infinite loop but, as we said above, when called, this function does not get executed until the “next” function of the returned “Generator” object is called.
Let’s call this method to get the “Generator” object:
Right now, we have an object called “generator” on which you can call the “next” function causing the function to execute until the “yield” keyword is reached. Even better, you can also get the “value” property on the objects returned by the “next” function:
Executing this code produces the following result:
As you can see, Chrome does not get stuck because of the infinite loop. I’m gonna repeat it again to be sure that it’s clear: a generator function does not get executed when called but when the “next” function of the returned generator is called and pauses when it reaches the “yield” keyword.
So, in our example, when this function is called, a “Generator” object is returned and the function body does not get executed so, at that moment, no value is produced whatsoever. Then, calling the “next” function starts the execution of the function and produce a random number. As we are in a loop, the “yield” keyword is once again reached, so the function gets paused again (until another call to the “next” function of the “Generator”), and so forth.
As generator functions return an object containing a “next” function, it can be used as an iterable, so it’s possible to do something like this:
The “for … of” loops through all the value returned by the “Generator” object. Of course, as this one actually executes an infinite loop, we need to break the “for … of” at some point or we would really end up with an infinite loop. Indeed, “for … of” would keep calling the “next” function to get the next value, so the “while” loop would be executed infinitely.
To be even more precise, it’s worth mentioning that every object returned by the “next” function of the generator would have the following shape:
Now, we could also break the loop from inside the generator function by using the “return” keyword.
This generator function would behave as the previous one except that when the generated number equals the value of the third parameter, the “return” keyword gets hit, terminating the generator. This time, the shape of the returned object would be:
As the “done” property of this object is “true”, the “for … of” loop stops its execution.
Note that once the execution of a generator is stopped, it can’t be restarted, meaning that doubling the “for … of” loop wouldn’t produce two sets of random values. Indeed, the second loop would try to use a completed generator, so no value would be produced.
It is also possible to leverage the value returned by the “yield” keyword inside the generator function. For that, you just have to pass a value to the “next” function:
At line 16, we call the “next” method of the “Generator” object by passing “stop” in parameter. This value gets retrieved at line 5 when calling the “yield” keyword. If this value equals “stop” (which is the case for the second execution), the loop is broken and the generator stops using the “return” keyword.
Executing this code produce an interesting result:
We could think that it’s a coincidence that both numbers are the same but you can keep on refreshing the page, the two numbers would also be the same. This enlightens how generators are working.
Creating the “Generator” object by calling the function does not execute the function.
This means that in our example, calling “random” does not execute the function, so the “while” loop does not even start. Then, calling the first “next” function generates a random number, returns it and pauses the function. This means that the first value returned by the “yield” inside the generator function will actually be the parameter sent to the second “next” call.
This explains why the two numbers are always the same. As the function pauses at the first “yield” (after having returned the first value) and gets resumed when calling the second “next” with the “stop” value, the condition breaking the loop gets executed directly and returns the previously generated number which means that a new random number does not get generated.
Using generators to make iterators easier
Now that we learned this, let’s have another look at the “iterator” created in the “Make your ES6 classes iterable” post.
You normally now have the knowledges required to replace the iterator function by a generator:
We simply loops through the “_drivers” array and return each value of it. Don’t forget to add the star before the function name to specify that it is a generator one. Once the end of the array is reached, the value returned by the generator is:
This value causes the “for … of” loop to stop its execution.
Note that using a “generator” to wrap the “iterator” frees us from having to maintain the internal state of the loop. We just have to loop through our array and yield the different values.