Generators were introduced in ES6, and are available on these platforms. While I have not used generators in the browser yet, I use them heavily in server-side iojs.
Why should I care?
In my opinion, the number one reason to use generators is to clean up asynchronous code. Generators can also be used to create array-like objects, but their interactions with promises are incredibly powerful. This article will explain generators; a future article will explain how it applies to cleaning up asynchronous code. For now, I want to take you through the unique ways in which generators differ from normal functions.
Overview
First things first, here is a quick overview of how generators work. Some of this might not make sense yet, so take a quick glance and then read the full tutorial below.
Generator
Example 1
function* doStuff(value) {
var foo = yield value;
return foo;
}
Some notes about the generator:
- Must be declared as
function*
(with an asterisk). - Should contain one or more
yield
statements. - Returns an iterator, not the function’s return value.
- Starts in a paused state until you call the
next()
method of the iterator. yield
will also pause execution of the generator until the iterator allows it to continue via the next()
method.- To pass data out of the generator, you must
yield
or return
your value. This value will be part of the object returned by the iterator’s next
method.
Iterator
Example 2
var iterator = doStuff("banana");
while (!iterator.done) {
iterator.next();
}
Some notes about the iterator returned by a generator:
- The most important method is
next()
, which will resume execution of the generator until it hits the next yield
statement, or the function has completed its execution. - To pass data into your generator (eg
"banana"
), provide it as a parameter to next()
on the iterator. It will be the return value of a yield statement. This is optional. - Each call to
next()
returns an object with two properties, value
and done
. value
contains the current value of the iterator. In this case the yielded value.done
will be true
if the function has completed execution.- If you want
yield
to throw an exception instead of returning a value, your iterator can use the throw()
method.
Ok, tell me more
Generators are different from normal functions in four ways:
- Generators must contain an asterisk (
*
) next to the function keyword (e.g. function* doStuff()
). This defines the function as a generator, instead of a normal function. - Generators can contain
yield
statements. (e.g. var x = yield foo();
). - Generators do not return your return value, they return an iterator.
- Generators are not executed at the time they are invoked.
Yield
Before we go into why or how we use a yield
statement, let’s just talk about the syntax. The following example is a fairly basic line of code. We will compare that line to one with a yield
statement
Example 3
result = encodeURIComponent("http://www.dashron.com");
As you are probably aware, the above code is executed in two easy steps:
- The assignment operator (
=
) requires a value on the right, so encodeURIComponent
is called with a parameter. - The assignment operator then puts the return value of
encodeURIComponent
into the variable, result
.
So, what happens if you add a yield
statement?
Example 4
result = yield encodeURIComponent("http://www.dashron.com");
At this level, yield
acts a bit like an assignment operator.
- The assignment operator requires a value on the right, so we have to process the statement
yield encodeURIComponent("http://www.dashron.com")
. - The
yield
statment also requires a value on the right, so encodeURIComponent("http://www.dashron.com")
is executed with the string parameter. yield
takes the return value of encodeURIComponent()
, performs a little bit of magic (more on this later), and passes a value to the assignment operator.- The assignment operator then puts the return value of the
yield
statement into the variable, result
.
Note: Unlike the assignment operator, yield
does not need a variable to its left. Like a function, you can use parenthesis to interact with the return value in place. For example, the following is valid:
Example 5
result = (yield encodeURIComponent("http://www.dashron.com")).length;
So what can yield
do? A lot actually. It’s a little complicated, so let’s go over it step by step.
Now things get a little weird
yield
pauses your function, and allows you to resume execution at any time. I want to get that out of the way first, because it’s not something you see outside of generators. In fact, you don’t even need to use yield
, generators always start out paused. To see how this works, let’s check out a generator example without any yield
statements:
Example 6
function* doStuff() {
return "Noses on dowels";
}
var result = doStuff();
var nextResult = result.next();
In Example 6, result
does NOT equal "Noses on dowels"
. result
contains an iterator. This object is the “remote control” of your generator. It has a single method, next()
. Every time you call next()
on your iterator, the function will execute up until: (1) it encounters a yield
statement; or (2) the function has finished execution. Here, result
contains your iterator, and nextResult
contains contains information about the current iteration.
Now let’s add a couple of yield
statements into the mix:
Example 7
function* doStuff() {
var catchphrase = yield "Didja get that thing I sent you";
var finalphrase = yield catchphrase;
return finalphrase;
}
var result = doStuff();
var nextResult = result.next().value;
var secondResult = result.next("Blackwatch Plaid");
var finalResult = result.next("Happy Cake Oven");
Each time you call next()
, it executes part of the doStuff()
function. Let’s break down Example 7 into each call to next()
.
The first call to next()
Any time you call next()
it behaves identically, except for the first and last time. Let’s walk through each next()
call in order, starting with var nextResult = result.next();
. This call will execute the code shown in example 7.1.
Example 7.1
yield "Didja get that thing I sent you";
Notice that the code to the left of the yield
statement (var catchphrase =
) is not shown in Example 7.1, becasue it is not executed at this time. That’s because the yield
statement pauses execution before it can happen! You must interact with your iterator to continue to the rest of the code. So let’s review the second next()
call, var secondResult = result.next("Blackwatch Plaid");
. This call will execute the code shown in Example 7.2.
The standard call to next()
Example 7.2
var catchphrase = yield
yield catchprase;
The first line of code in Example 7.2 needs to assign a value to the variable catchphrase
. The assignment operator is expecting a value from the yield
statement, and this value is provided by the iterator’s next()
method. Example 7.2‘s code is executed when you call result.next("Blackwatch Plaid");
, so yield
returns "Blackwatch Plaid"
.
Example 7.2 above is important, and worth re-reading. This is the standard behavior of a iterator’s next()
method. Every time you call next()
, a chunk of your generator will be executed, until there is no code left to run. next()
will fail if there is no code left, so you need to keep track of one more piece of information: the done
parameter.
The third (and final) call to next()
Example 7.3 demonstrates the final code in this generator’s execution.
Example 7.3
var finalphrase = yield
return finalphrase;
This contains everything that is executed between the final yield
and return
statements. In example 7, code is run the third time next()
is called. Calling next()
a fourth time is not terribly useful, it will return the same value as the third, without executing any code. To make sure you don’t call next()
unnecessarily you need to keep an eye on the return values of next()
. Each time next()
is called it returns an object with two properties.
value
: This depends on the execution. If this is not the final next statement, it will contiain the yielded value. If this is the final next statement, it will contain the returned valuedone
: true
if the generator has completed execution. false
otherwise.
So if done
is true, you should stop calling next()
.
Example 7 did not make use of the done
property because it wasn’t necessary. done
is used most commonly in more complex code, so let’s jump into our final example.
Yield with loops
Example 8
function* getTen() {
for (var i = 0; i < 10; i++) {
yield i;
}
}
var gen = getTen();
Notice that the generator in Example 8 only has one visible yield
statement. This does not mean that the function execution will only be paused once. Becuase the yield
is inside a for
loop, each iteration of the loop will reach the yield
and pause execution. This specific function will pause execution 10 times, sending out a number each time (0 through 9).
To properly execute the generator you will need to call the next()
method many times. I’m lazy, and I don’t want to copy the next()
method over and over again. Instead, we can throw next()
into a loop and check return value each time. next
returns the object mentioned above (with example 7.3), so you should watch it’s done
property. As long as it evaluates to false
, we can continue to call this iterator’s next()
method.
Example 9
var progress = null;
do {
progress = gen.next();
console.log(progress.value);
} while(!progress.done);
And now we’re done! Your generator will be processed completely, hitting every yield
statement until the function is complete. But what does this have to do with asynchronous code and callbacks? I will be writing more on that in the near future, so check back soon!