CHAPTER 11
Generators are functions that can be paused and resumed. They simplify iterator-authoring using the function* and yield keywords. A function declared as function* returns a Generator instance. Generators are subtypes of iterators that include additional next and throw functions. These enable values to flow back into the generator, so yield is an expression form which returns or throws a value.
As stated in the previous paragraph, generators are functions that can be paused and resumed. Let’s take a look at the following simple example to get a better understanding of generators:
Code Listing: 145
function* sampleFunc() { console.log('First'); yield; console.log('Second'); } let gen = sampleFunc(); console.log(gen.next()); console.log(gen.next()); |
Here, we have created a generator named sampleFunc. Notice that we are pausing in the middle of the function using the yield keyword. Also notice that when we call the function, it is not executed. It won’t be executed until we iterate over the function, and so here we iterate over it using the next function. The next function will execute until it reaches a yield statement. Here it will pause until another next function is called. This in turn will resume and continue executing until the end of the function.
Let’s look at the output for further illustration:
Code Listing: 146
First { value: undefined, done: false } Second { value: undefined, done: true } |
Now let’s move onto the next section and look at generator iteration.
Let’s look at an example of creating a generator and then iterating using it. Consider the following code example:
Code Listing: 147
let fibonacci = { [Symbol.iterator]: function*() { let pre = 0, cur = 1; for (;;) { let temp = pre; pre = cur; cur += temp; yield cur; } } } for (let n of fibonacci) { // truncate the sequence at 100 if (n > 100) break; console.log(n); } |
As you can see from the preceding code, generators are indeed a subtype of iterators. The syntax is very similar, with just a few changes. Notably, you see the function* and yield keywords.
The following is the output for the preceding code:
Code Listing: 148
1 2 3 5 8 13 21 34 55 89 |
As you can see, we get the same output as we did when we wrote our own iterator.
Let’s write this one more time using some more ES6-style coding this time:
Code Listing: 149
let fibonacci = { *[Symbol.iterator]() { let pre = 0, cur = 1; for (;;) { [ pre, cur ] = [ cur, pre + cur ]; yield cur; } } } for (let n of fibonacci) { if (n > 100) break; console.log(n); } |
As you can see, the asterisk (*) is now on the [Symbol.iterator] definition. Also note the usage of destructuring of objects. This makes for even terser and cleaner code.
The following is the output for the preceding code:
Code Listing: 150
1 2 3 5 8 13 21 34 55 89 |
Let’s consider another example where we use the generator directly without the iterator:
Code Listing: 151
function* range (start, end, step) { while (start < end) { yield start; start += step; } } for (let i of range(0, 10, 2)) { console.log(i); } |
Here, we are defining a generator that takes start, end, and step as arguments. We pause when we reach yield. Next, we perform addition to start in the amount of step. This continues until we reach end.
The following is the output for the preceding code:
Code Listing: 152
0 2 4 6 8 |
It is possible to use generators and support destructuring via array matching. Let’s look at the following example:
Code Listing: 153
let fibonacci = function* (numbers) { let pre = 0, cur = 1; while (numbers-- > 0) { [ pre, cur ] = [ cur, pre + cur ]; yield cur; } } for (let n of fibonacci(5)) console.log(n); let numbers = [ ...fibonacci(5) ]; console.log(numbers);; let [ n1, n2, n3, ...others ] = fibonacci(5); console.log(others[0]); |
In the preceding code, we are first creating a generator. Next, we perform a simple iteration over the generator for five iterations. Next, we use the spread operator to assign values to the numbers array. Finally, we use pattern matching in the array and assign others the values from the generator function.
Here is the output for the preceding code:
Code Listing: 154
1 2 3 5 8 [ 1, 2, 3, 5, 8 ] 5 |
One of the promises of generators is control flow. This becomes important when dealing with asynchronous programming. We see this a lot with promises, which we will be looking at in another chapter. Let’s take a look at how we can handle control flow while using generators. Take a look at the following example:
Code Listing: 155
function async (proc, ...params) { var iterator = proc(...params); return new Promise((resolve, reject) => { let loop = (value) => { let result; try { result = iterator.next(value); } catch (err) { reject(err); } if (result.done) { resolve(result.value); } else if (typeof result.value === "object" && typeof result.value.then === "function") result.value.then((value) => { loop(value); }, (err) => { reject(err); }) else { loop(result.value); } }; loop(); }) } // application-specific asynchronous builder function makeAsync (text, after) { return new Promise((resolve, reject) => { setTimeout(() => resolve(text), after); }) } // application-specific asynchronous procedure async(function* (greeting) { let foo = yield makeAsync("foo", 300); let bar = yield makeAsync("bar", 200); let baz = yield makeAsync("baz", 100); return `${greeting} ${foo} ${bar} ${baz}`; }, "Hello").then((msg) => { console.log(msg); }); |
Okay, let’s break this down. The first function, async, receives a generator as the first argument and any other arguments that follow. In this case, it receives Hello. A new promise is created that will loop through all the iterations. In this case, it will loop exactly four times. The first time, the value is undefined and we call iterator.next() in passing in the value. All subsequent times, value represents foo, bar, and baz. We complete looping once we have exhausted all of our iterations.
The next function, makeAsync, simply takes some text and waits a set period of time. It uses the setTimeout function to simulate a real world scenario.
The last code segment is our execution of the async function. We pass in a generator function that takes in a greeting as an argument as well as contains three yield statements and a return. It makes the call to async and executes the iterations while adhering to the various setTimout specifications. The nice thing here is that regardless of when each of the iteration calls return, we still have code that looks very synchronous in manner. We do not display our message until the iteration has been completed.
Here is the output from the preceding code:
Code Listing: 156
Hello foo bar baz |
Generator methods are the support for methods in a class and on objects based on generator functions. Consider the following example:
Code Listing: 157
class GeneratorClass { * sampleFunc() { console.log('First'); yield; console.log('Second'); } } let gc = new GeneratorClass(); let gen = gc.sampleFunc(); console.log(gen.next()); console.log(gen.next()); |
Just like our first example, we are now using generators inside our ES6 classes. Also like the first example, we get the same output:
Code Listing: 158
First { value: undefined, done: false } Second { value: undefined, done: true } |