Generator Functions

Consider the eager and lazy range function implementations. We lose a certain clarity when we convert the array range maker into an iterator range maker. Generator functions alleviate this problem by offering a way to express iterations procedurally, with a lazy behavior.

A JavaScript engine near you may already support generator functions. The syntax consists of adding an asterisk to the function declaration and using yield to produce iterations. Calling a generator function does not execute the function, but instead sets up a state machine to track where we are in the function and returns an iterator. Whenever we ask the iterator for an iteration, the state machine will resume the execution of the function until it produces an iteration or terminates.

function *range(start, stop, step) {
    while (start < stop) {
        yield start;
        start += step;
    }
}

var iterator = range(0, Infinity, 1);
expect(iterator.next().value).toBe(0);
expect(iterator.next().value).toBe(1);
expect(iterator.next().value).toBe(2);
// ...

Notice that the range generator function restores and perhaps even exceeds the clarity of the range array maker.

Calling next has three possible outcomes. If the iterator encounters a yield, the iteration will have a value. If the iterator runs the function to either an express or implied return, the iteration will have a value and done will be true. If the iterator runs to an explicit return, this terminal iteration carries the return value. If the generator function throws an error, this will propagate out of next().

Generators and iterators are unicast. The consumer expects to see every value from the producer. Since generators and iterators cooperate, information flows both forward as values, and backward as requests for more values.

However, the consumer can send other information back to the producer. The next method, familiar from basic iterators, gains the ability to determine the value of the yield expression from which the generator resumes. As a trivial example, consider a generator that echoes whatever the consumer requests.

function *echo() {
    var message;
    while (true) {
        message = yield message;
    }
}

var iterator = echo();
expect(iterator.next().value).toBe(undefined);
expect(iterator.next("Hello").value).toBe(undefined);
expect(iterator.next("Goodbye").value).toBe("Hello");
expect(iterator.next().value).toBe("Goodbye");
expect(iterator.next().value).toBe(undefined);

We must prime the generator because it does not begin with a yield. We advance the state machine to the first yield and allow it to produce the initial, undefined message. We then populate the message variable with a value, receiving its former undefined content again. Then we begin to see the fruit of our labor as the values we previously sent backward come forward again. This foreshadows the ability of stream readers to push back on stream writers.

Additionally, the iterator gains a throw method that allows the iterator to terminate the generator by causing the yield expression to raise the given error. The error will unravel the stack inside the generator. If the error unravels a try-catch-finally block, the catch block may handle the error, leaving the generator in a resumable state if the returned iteration is not done. If the error unravels all the way out of the generator, it will pass into the stack of the throw caller.

The iterator also gains a return method that causes the generator to resume as if from a return statement, regardless of whether it actually paused at a yield expression. Like a thrown error, this unravels the stack, executing finally blocks, but not catch blocks.

As such, like next, the throw and return methods may either return an iteration, done or not, or throw an error. This foreshadows the ability of a stream reader to prematurely stop a stream writer.

iterator.throw(new Error("Do not want!"));

Note that in Java, iterators have a hasNext() method. This is not implementable for generators owing to the Halting Problem. The iterator must try to get a value from the generator before the generator can conclude that it cannot produce another value.

results matching ""

    No results matching ""