Generators

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:

  1. The assignment operator (=) requires a value on the right, so encodeURIComponent is called with a parameter.
  2. 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.

  1. The assignment operator requires a value on the right, so we have to process the statement yield encodeURIComponent("http://www.dashron.com").
  2. The yield statment also requires a value on the right, so encodeURIComponent("http://www.dashron.com") is executed with the string parameter.
  3. 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.
  4. 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 value
  • done: 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!