Advanced Generators

Chapter 15 23 mins

Learning outcomes:

  1. Advanced aspects of generators
  2. The return() and throw() methods
  3. Generator delegation

Introduction

In the previous chapter we learnt the basics of generator functions in JavaScript, that is how to create them; what's their syntax; how does the yield keyword work and so on and so forth.

However, owing to the fact that generators are such a complex topic, we didn't explain all their important aspects in the previous chapter.

In particular, we didn't understand what is generator delegation, or how to use the methods return() and throw().

Both these idea will be discussed in this very chapter which explores the advanced aspect of generators in advanced JS.

Surely, what we're about to learn isn't going to be a piece of cake! No, seriously, it isn't going to be that. Rather, it's going to be a whole cake!

Generator methods

In the previous chapter, we saw the next() method in some heroic action, where it resumed execution in the generator.

However, next() isn't the only method that is provided to Generator objects.

We have two other methods: return() and throw().

The good news is that these methods operate exactly how their names sound! Let's discuss them.

return()

The return() method simply returns the iterator with a given value. Returning means that we are done iterating, likewise done is set to true.

When return() is called, no matter how long the sequence remains, it is terminated with the value provided to the method.

See how this method works much the same way the return keyword does - the keyword returns a given value to the calling context and ignores everything that follows. Quite the same goes for return() as well!

Consider the code below. It's the same positiveInts() generator function that we created above:

function* positiveInts() {
    for (var i = 0; true; i++) {
        yield i;
    }
}

var seq = positiveInts();

Let's call the method next() a couple of times and see what do we get...

seq.next(); // {value: 1, done: false}
seq.next(); // {value: 2, done: false}
seq.next(); // {value: 3, done: false}

As expected, we get subsequent values of the sequence defined by the positiveInts() function.

Now after the three next() calls above, let's call the return() method and see what do we get this time:

seq.return("Bye"); // {value: "Bye", done: true}

As we said before, calling return() on a generator object closes it down i.e sets done to true, and value equal to the argument provided.

This is exactly what's happening here as well.

We call seq.return("Bye") which returns an object with done equal to true and value equal to the string "Bye".

Once an iterator is done (i.e the done property is true), further next() calls will simply return the object {value: undefined, done: true}.

A call to return() will always behave the same way, no matter how many times you've called it before. That is,

It'll always return an object whose value property is equal to the argument provided, while its done property is equal to true.

Following is an illustration, using the same positiveInts() generator function created above:

seq.return(3); // {value: 3, done: true}
seq.return(4); // {value: 4, done: true}

We call return(3) for the first time in line 1 which results in the object {value: 3, done: true} being returned.

After that, we call return(4) in line 2 which as expected, results in the object {value: 4, done: true} being returned; NOT the object {value: undefined, done: true} that is otherwise returned for subsequent next() calls.

throw()

Moving over to the other method throw(), we see that it also behaves quite how its name makes us think it does!

throw() simply resumes execution inside the generator and throws an error in there, where the corresponding yield dwells.

If we catch the exception using a try...catch statement, and the catch() block has a yield within it as well, throw() will then return the same object next() does.

If execution begins in the generator i.e no next() method has yet been called; or if the generator is done i.e the done property is true, then calling throw() will result in an uncaught exception.

Behind the scenes, throw() actually executes a throw statement with the provided argument, which means that we can also encapsulate its call in a try block to catch the respective exception. We'll see this shortly.

Let's decrypt what all this means...

Consider the code below:

function* gen() {
    try { yield 10; }
    catch(e) {
        yield "Error Caught";
    }
}

var seq = gen();
seq.next(); // {value: 10, done: false}

When we call seq.next() for the first time, we obviously get {value: 10, done: false} returned from the first yield statement. After this we know that if execution resumes inside the generator, it'll resume from this very point.

Now, calling seq.throw() resumes execution inside the generator and acts as if a throw keyword is put in the location where the previous yield paused execution.

seq.throw();

This seq.throw() call can be thought of as the following:

function* gen() {
    try { yield 10 throw undefined; }
    catch(e) {
        yield "Error Caught";
    }
}

It resumes execution from the point where yield had previously paused it and throws an exception right at that point.

undefined comes from the fact that no argument is passed to seq.throw() - the argument is the value of the thrown exception, and likewise the parameter of catch().

This exception thrown inside the try block causes execution to shift in the corresponding catch block where the statement yield "Error Caught" is encountered.

Hence we get the object {value: "Error Caught", done: false} returned.

Whenever the JavaScript interpreter encounters a yield statement within a generator, it returns the object {value: yieldedValue, done: trueOrFalse} to the calling context, which could be the next() or throw() method.

Now if we call seq.next(), we get done as true since the generator function has reached its end, with value equal to undefined:

seq.next(); // {value: undefined, done: true}

Let's now consider some argumented throw() calls.

With the following code in place,

function* gen() {
    try { yield "foo"; }
    catch(e) {
        yield e;
    }
}

var seq = gen();

if we now execute the two statements below, see closely what the throw() call returns:

seq.next(); // {value: 10, done: false}
seq.throw("OK"); // {value: "OK", done: false}

seq.next(); // {value: undefined, done: true}

Let's look at another example, in fact the same example above with a different sequence of next() and throw() calls.

Suppose we have the following code:

function* gen() {
    try { yield 10; }
    catch(e) {
        yield "Error Caught";
    }
}

var seq = gen();

Now let's start by calling seq.throw() (as opposed to calling seq.next() first, which we did in the previous example).

seq.throw();

As before, this throws an exception inside the generator, but this time since we are starting out in the generator and there isn't any yield statement that can be replaced, an exception is thrown right at the start of the generator.

You can think of it as follows:

// first calling seq.throw()
function* gen() {throw undefined
    try { yield 10; }
    catch(e) {
        yield "Error Caught";
    }
}
Just remember that wherever execution resumes inside a generator function, is the point where throw() will put a throw expression in it.

In this case calling seq.throw(), to start with, resumes execution right from the start of the generator function and thus puts the expression throw undefined exactly there.

Because of the fact that this exception isn't encapsulated inside a try block, it's logged in the console as an uncaught exception.

Below shown is what happens on first calling seq.throw():

Calling seq.throw()
Calling seq.throw()

When an uncaught exception occurs in a generator, it is closed which means that further next() calls will return the object {value: undefined, done: true}.

Consider the code below, executed after making the seq.throw() call above, which threw an uncaught exception:

seq.next(); // {value: undefined, done: true}
seq.next(); // {value: undefined, done: true}

Each seq.next() returns an object with done equal to true, just because the generator seq has been closed.

Once a generator is closed, done evaluates to true. Remember this!

One final thing to discuss with regards to throw() is that the generator's resumation and exception-throwing all occurs within its own definition.

What this basically means is that to handle any exceptions, we can also encapsulate the call to throw() directly inside a try block, rather than putting the try block inside the generator.

Take a look at the code below:

function* gen() {
    yield 100;
}

var seq = gen();

try {
    seq.throw();
}
catch (e) {
    console.log("Error Caught");
}

The seq.throw() statement in line 8 will definitely throw an error, but that error would now be caught by the catch block in line 10, ultimately logging "Error Caught".

Error Caught

However note that since the thrown exception isn't caught from inside the generator, it's nonetheless still closed.

Only if the exception is caught from within the generator, does everything continue normally! Otherwise, the generator is closed.

Delegating generators

As we saw in the previous chapter, multiple yields inside a generator function, each define the next value in a whole sequence of values.

For example, the following code has three yield keywords and likewise defines a three-item sequence.

function* threeItemSeq() {
    yield 1;
    yield 2;
    yield 3;
}

Now this works well in cases where you want to yield individual values crafted right in the generator.

However for cases where you want to yield individual values not crafted in your generator, but rather predefined in an iterable like an array, things start to become a bit monotonous.

Consider the code below. We want to iterate over an array arr and yield each of its elements one-by-one:

function* gen() {
    var arr = [1, 2, 3];
    for (var i = 0, len = arr.length; i < len; i++) {
        yield arr[i];
    }
}

Undoubtedly, this isn't a difficult task to do, but as you might agree, not so cool!

For a more vivid example consider the following.

This time we have to iterate over two separate arrays, or better to say two iterables. And for each iterable, we need to create a separate loop and iterate over the iterable one-by-one.

If we have four such iterables then surely we'll need four such loops.

Fortunately, JavaScript has a solution for this too - use the yield* expression.

The yield* expression is a special type of the yield keyword that works much like the loops shown above.

It iterates over a given sequence and yields each of its values one-by-one.

What we get as a result is an extremely elegant notation to yield individual items from an iterable.

The same example of iterating over the array arr above, can be rewritten using yield* as shown below:

function* gen() {
    var arr = [1, 2, 3];
    yield* arr;
}

What yield* does over here is that it iterates over the iterable arr and yields each of its elements one-by-one.

Behind the scenes, yield* translates to the following code for a given iterable o:

function* gen() {
    // yield* o
    // is the same as
    for (var item of o) {
        yield o;
    }
}
The yield* expression is nothing new it's just syntactic sugar over the yield keyword to simplify working with an iterable.

Now for the exciting part, recall that generators are also iterable in nature and so we can provide generator expressions to yield* inside another generator.

In other words, we can work with generators inside generators.

This is what we call generator delegation.

Let's see an example...

Suppose we have three generators; one that defines the sequence of whole numbers from 1-5; one that defines the sequence of square numbers from 1-50, and one that defines the sequence of cube numbers from 1-100.

Using these three generators we can create a fourth generator, by means of delegation, which will define all the three sequences given above in one deal.

Following is the code for the three generators:

// defines the sequence 1, 2, 3, 4, 5
function* wholeNums() {
    for (var i = 1; i <= 5; i++) {
        yield i;
    }
}

// defines the sequence 1, 4, 9, 16, 25, 36, 49
function* squareNums() {
    for (var i = 1, s = 1; s <= 50; i++, s = i ** 2) {
        yield s;
    }
}

// defines the sequence 1, 8, 27, 64
function* cubeNums() {
    for (var i = 1, c = 1; c <= 100; i++, c = i ** 3) {
        yield c;
    }
}

And following is the code for the fourth generator, combining all the three generators above:

function* gen() {
    yield* wholeNums();
    yield* squareNums();
    yield* cubeNums();
}

Now let's invoke this generator function gen() and inspect its sequence.

var seq = gen();

seq.next(); // {value: 1, done: false}
seq.next(); // {value: 2, done: false}
.
.
.
seq.next(); // {value: 1, done: false}
seq.next(); // {value: 4, done: false}
.
.
.
seq.next(); // {value: 1, done: false}
seq.next(); // {value: 8, done: false}

Continuously calling seq.next() here will operate as explained below.

The first yield* expression in line 2 will complete first and only then will execution move to line 3. One-by-one it'll yield all the values defined by wholeNums() and then complete.

After this the second yield* expression will be given attention before allowing execution to move to line 4. This second yield* expression will, one-by-one, yield all the values defined by squareNums() and then complete.

Finally, the last yield* expression will execute, yielding all the values defined by cubeNums().

After the last yielded value which is 64, the generator gen() completes and thus we get the object {value: undefined, done: true} returned.

And, in effect, this is generator delegation. An extremely powerful concept to further strengthen the already-powerful concept of generators! Amazing.

Now if you feel you've really understood all this well enough, including the concept of iterators and iterables, try solving the task below.

Write a one-line statement that logs the entire sequence defined by gen() above, as a string value with ", " delimiters appearing between individual values.

Your code should look something like this:

console.log( /* your code */ );

And the log should be the following:

1, 2, 3, 4, 5, 1, 4, 9, 16, 25, 36, 49, 1, 8, 27, 64

You need to use an operator or a static method.

What we need to do in this task is to just put the sequence defined by gen() inside an array, so that we can easily call its join() method on the array to join all its individual elements by the ", " delimiter.

Now there are two ways to get gen() fill up an array: one is to use the spread operator while the other one is to use the Array.from() method.

Both these way operate on iterables, pushing each of the iterable's items onto an array. And once we have an array, we just need to just call join(", ") to join the elements into a string.

Following are the two one-line solutions to this task. First with the spread operator:

console.log([...gen()].join(", "));

And now with the Array.from() method:

console.log(Array.from(gen()).join(", "));