Hi everyone, and welcome to another exciting edition of Boring JavaScript! Today, we tackle a subject that I’ve wanted to tackle for many a month now – GENERATOR FUNCTIONS!
Don’t like to read? Then check out our video.
Don’t Fly A Kite During a Thunderstorm
First, what is a generator function? Simply, it’s a function that allows you to define a custom iterator.
What’s an iterator? That’s an object that allows you to iterate over a given series or collection.
ELI5: Take an array – any array. It is a ‘series’ or ‘collection’ of data. You can use methods like .forEach(), .map(), .filter(), and the like to go through each element in the array and do something funky with it. Internally, JavaScript uses an iterable object to control which element it will pass to you next. It’s something you don’t worry about, but it’s used all the time within JavaScript when traversing – or ‘iterating’ over collections like Maps or Arrays.
Generator Functions (or Generator Methods on a class) are functions that let you define a custom iterable so that other applications can easily traverse whatever collection you want to iterate with simple methods. It’s a very, very powerful way to expose complex data patterns as simple collections.
And they are easy to build and use.
I Yield to the Congressperson from Missouri
Defining a generator function is really, really simple:
const myAnimals = function* () {
yield 'Cat';
yield 'Dog';
yield 'Horse';
}
const allAnimals = myAnimals();
let animal = allAnimals.next();
while (!animal.done) {
console.log(animal.value);
animal = allAnimals.next();
}
/* output
Cat
Dog
Horse
*/
Notice Line 1. Notice the asterisk by the word ‘function’? That tells JavaScript this is a Generator Function. That’s it. Nothing else.
There are three important points to remember about Generator Functions:
1. The Function is NOT executed immediately
When a generator function is ‘called’ (Line 7 in our example), the value assigned to the the variable (‘allAnimals’ in our example) is not the final ‘returned’ value from the function. That returned value is a generator object, which is the iterable the consuming code will need to iterate over the collection. This also means that the function itself – unlike a ‘normal’ function – is not executed when it is ‘called’ (as in Line 7).
So if it’s not executed immediately, when is it executed? That will be the first time the consuming program requests data. Requesting the data is as simple as executing the .next() method on the generator object. As seen in our code:
const allAnimals = myAnimals(); // myAnimals() is NOT executed
let animal = allAnimals.next(); // myAnimals() is first executed at this point
Why is that? Well – why execute the function if you don’t need any data?
And how do you get that data?
2. Data is ‘yielded’ to the consumer when the .next() method is called and execution of the function is suspended
Let’s look at the function again:
const myAnimals = function* () {
yield 'Cat';
yield 'Dog';
yield 'Horse';
}
Notice the ‘yield’ statement in lines 2, 3, and 4. This is how JavaScript passes data from the generator function to the calling program. When the first ‘next()’ method is called, the generator function begins execution. When it hits a ‘yield’ statement, the function is suspended – meaning execution of the function ceases at that point but the function has not finished execution. It is merely paused. When another ‘next()’ method is called, execution resumes from that yield statement until another yield statement is encountered. Basically:
allAnimals.next(); // begins execution and returns 'Cat'
allAnimals.next(); // resumes execution, and returns 'Dog';
allAnimals.next(); // resumes execution, and returns 'Horse';
How is the data returned to the calling program? ‘yield’ will return an object with two properties: ‘value’ and ‘done’. ‘value’ will contain the data after the ‘yield’ statement – in our case, ‘Cat’, ‘Dog’, and ‘Horse’. ‘done’ is a Boolean indicating if there is more data or not to get from the collection.
And how does it know when the end is reached?
3. Iteration of the collection is completed when the function ‘returns’.
As long as the calling program calls ‘next()’, data will be returned, and the ‘done’ property on the returned data will be Boolean False. It will set itself to Boolean True when a ‘return’ statement is encountered in the function.
Let’s look at two examples:
const myAnimals = function* () {
yield 'Cat';
yield 'Dog';
yield 'Horse';
}
const myAnimals = function* () {
yield 'Cat';
yield 'Dog';
return;
yield 'Horse';
}
In our first example, ‘return’ is implied at the end of the function (as it is will all functions). So once the ‘yield’ for ‘Horse’ is sent over to the calling program, and another ‘next()’ is executed, the function continues on, and encounters the end of the function. This is, again, an implicit ‘return’, so the object sent back has the done property set to Boolean True. And in that way the calling program knows there is no more data.
In our second example, we have the ‘return’ right after the ‘yield’ for ‘Dog’. This means that ‘Horse’ will never be sent to the calling program, because the ‘return’ statement always signifies the end of data.
Customizing Your Iterators
The above examples are easy. How about something complex? How about this data?
const animals = {
1: { "type": "Cat", "name": "Fluffy", "classification": "Mammalia" },
2: { single: { "type": "Dog", "name": "Fido", "classification": "Mammalia" }},
3: { "type": "Horse", "name": "Mr. Ed", "classification": "Mammalia" },
4: { single: { double: { "type": "Cow", "name": "Betsy", "classification": "Mammalia" }}},
5: { single: { double: { "type": "Coyote", "name": "Wile E.", "classification": "Mammalia" }}},
6: { single: { double: { triple: { "type": "Road Runner", "name": "Beep Beep", "classification": "Aves" }}}},
7: { "type": "Dolphin", "name": "Flipper", "classification": "Mammalia" },
8: { single: { "type": "Whale", "name": "Moby Dick", "classification": "Mammalia" }},
9: { single: { double: { "type": "Lizard", "name": "Larry", "classification": "Reptilia" }}}
};
Talk about a mess. But in using generators, you can ‘normalize’ the data sent to the calling program, no matter how the data is formatted. Let’s look at what a generator for the above might be:
const processData = function* () {
const keys = Object.keys(animals);
for (let i = 0, l = keys.length; i < l; i++) {
const animal = animals[keys[i]];
let type, name, classification;
switch(keys[i]) {
case '1':
case '3':
case '7':
({ type, name, classification } = animal);
break;
case '2':
case '8':
({ type, name, classification } = animal.single);
break;
case '4':
case '5':
case '9':
({ type, name, classification } = animal.single.double);
break;
case '6':
({ type, name, classification } = animal.single.double.triple);
break;
}
yield { type, name, classification };
}
}
It’s a mess, yes, but it’s an organized mess. We’re doing a simple ‘for’ loop to go through the data, then using a ‘switch’ statement to determine how to extract the ‘type’, ‘name’, and ‘classification’ from the data. Finally we do the ‘yield’ just inside the ‘for’ loop (Line 25).
This works because the first time the calling program does the ‘next()’ method, the function is executed. The ‘for’ loop begins, the first record is parsed, and the ‘yield’ statement sends the data back to the calling program. At this point, the function suspends, and no longer executes any code.
When the calling program issues another ‘next()’, the function resumes from where it left off, and the next index in the ‘for’ loop is processed, the next ‘yield’ is encountered, and the function suspends again until ‘.next()’ is issued from the calling program.
Eventually, the ‘for’ loop satifies itself, it exits, and the function ends. The generator will set the ‘done’ property to Boolean True for the final ‘next()’ method – because of the implicit ‘return’ at the end of the function.
Using this, you can now iterate anything you wish and all a calling program has to know is that you are an iterable object and use next() to get the data. You now have an amazing tool in your JavaScript arsenal.
The Video
As always, we have a video for this blog post. It goes into a real life example of how generators can be used.
Shameless Plug
Follow us everywhere!
Check out all our videos at: https://www.boringjavascript.com
Check out everything at: https://www.thevirtuoid.com
Facebook: https://www.facebook.com/TheVirtuoid
Twitter: https://twitter.com/TheVirtuoid
YouTube: https://www.youtube.com/channel/UCKZ7CV6fI7xlh7zIE9TWqgw
Categories: Boring JavaScript Javascript
thevirtuoid
Web Tinkerer. No, not like Tinkerbell.
Creator of the game Virtuoid. Boring JavaScript. Visit us at thevirtuoid.com
Leave a Reply