Please support this book: buy it (PDF, EPUB, MOBI) or donate

22. Generators

22.1 Overview

22.1.1 What are generators?

You can think of generators as processes (pieces of code) that you can pause and resume:

  1. function* genFunc() {
  2. // (A)
  3. console.log('First');
  4. yield;
  5. console.log('Second');
  6. }

Note the new syntax: function* is a new “keyword” for generator functions (there are also generator methods). yield is an operator with which a generator can pause itself. Additionally, generators can also receive input and send output via yield.

When you call a generator function genFunc(), you get a generator object genObj that you can use to control the process:

  1. const genObj = genFunc();

The process is initially paused in line A. genObj.next() resumes execution, a yield inside genFunc() pauses execution:

  1. genObj.next();
  2. // Output: First
  3. genObj.next();
  4. // output: Second

22.1.2 Kinds of generators

There are four kinds of generators:

  • Generator function declarations:
  1. function* genFunc() { ··· }
  2. const genObj = genFunc();
  • Generator function expressions:
  1. const genFunc = function* () { ··· };
  2. const genObj = genFunc();
  • Generator method definitions in object literals:
  1. const obj = {
  2. * generatorMethod() {
  3. ···
  4. }
  5. };
  6. const genObj = obj.generatorMethod();
  • Generator method definitions in class definitions (class declarations or class expressions):
  1. class MyClass {
  2. * generatorMethod() {
  3. ···
  4. }
  5. }
  6. const myInst = new MyClass();
  7. const genObj = myInst.generatorMethod();

22.1.3 Use case: implementing iterables

The objects returned by generators are iterable; each yield contributes to the sequence of iterated values. Therefore, you can use generators to implement iterables, which can be consumed by various ES6 language mechanisms: for-of loop, spread operator (), etc.

The following function returns an iterable over the properties of an object, one [key, value] pair per property:

  1. function* objectEntries(obj) {
  2. const propKeys = Reflect.ownKeys(obj);
  3.  
  4. for (const propKey of propKeys) {
  5. // `yield` returns a value and then pauses
  6. // the generator. Later, execution continues
  7. // where it was previously paused.
  8. yield [propKey, obj[propKey]];
  9. }
  10. }

objectEntries() is used like this:

  1. const jane = { first: 'Jane', last: 'Doe' };
  2. for (const [key,value] of objectEntries(jane)) {
  3. console.log(`${key}: ${value}`);
  4. }
  5. // Output:
  6. // first: Jane
  7. // last: Doe

How exactly objectEntries() works is explained in a dedicated section. Implementing the same functionality without generators is much more work.

22.1.4 Use case: simpler asynchronous code

You can use generators to tremendously simplify working with Promises. Let’s look at a Promise-based function fetchJson() and how it can be improved via generators.

  1. function fetchJson(url) {
  2. return fetch(url)
  3. .then(request => request.text())
  4. .then(text => {
  5. return JSON.parse(text);
  6. })
  7. .catch(error => {
  8. console.log(`ERROR: ${error.stack}`);
  9. });
  10. }

With the library co and a generator, this asynchronous code looks synchronous:

  1. const fetchJson = co.wrap(function* (url) {
  2. try {
  3. let request = yield fetch(url);
  4. let text = yield request.text();
  5. return JSON.parse(text);
  6. }
  7. catch (error) {
  8. console.log(`ERROR: ${error.stack}`);
  9. }
  10. });

ECMAScript 2017 will have async functions which are internally based on generators. With them, the code looks like this:

  1. async function fetchJson(url) {
  2. try {
  3. let request = await fetch(url);
  4. let text = await request.text();
  5. return JSON.parse(text);
  6. }
  7. catch (error) {
  8. console.log(`ERROR: ${error.stack}`);
  9. }
  10. }

All versions can be invoked like this:

  1. fetchJson('http://example.com/some_file.json')
  2. .then(obj => console.log(obj));

22.1.5 Use case: receiving asynchronous data

Generators can receive input from next() via yield. That means that you can wake up a generator whenever new data arrives asynchronously and to the generator it feels like it receives the data synchronously.

22.2 What are generators?

Generators are functions that can be paused and resumed (think cooperative multitasking or coroutines), which enables a variety of applications.

As a first example, consider the following generator function whose name is genFunc:

  1. function* genFunc() {
  2. // (A)
  3. console.log('First');
  4. yield; // (B)
  5. console.log('Second'); // (C)
  6. }

Two things distinguish genFunc from a normal function declaration:

  • It starts with the “keyword” function*.
  • It can pause itself, via yield (line B). Calling genFunc does not execute its body. Instead, you get a so-called generator object, with which you can control the execution of the body:
  1. > const genObj = genFunc();

genFunc() is initially suspended before the body (line A). The method call genObj.next() continues execution until the next yield:

  1. > genObj.next()
  2. First
  3. { value: undefined, done: false }

As you can see in the last line, genObj.next() also returns an object. Let’s ignore that for now. It will matter later.

genFunc is now paused in line B. If we call next() again, execution resumes and line C is executed:

  1. > genObj.next()
  2. Second
  3. { value: undefined, done: true }

Afterwards, the function is finished, execution has left the body and further calls of genObj.next() have no effect.

22.2.1 Roles played by generators

Generators can play three roles:

  • Iterators (data producers): Each yield can return a value via next(), which means that generators can produce sequences of values via loops and recursion. Due to generator objects implementing the interface Iterable (which is explained in the chapter on iteration), these sequences can be processed by any ECMAScript 6 construct that supports iterables. Two examples are: for-of loops and the spread operator ().
  • Observers (data consumers): yield can also receive a value from next() (via a parameter). That means that generators become data consumers that pause until a new value is pushed into them via next().
  • Coroutines (data producers and consumers): Given that generators are pausable and can be both data producers and data consumers, not much work is needed to turn them into coroutines (cooperatively multitasked tasks). The next sections provide deeper explanations of these roles.

22.3 Generators as iterators (data production)

As explained before, generator objects can be data producers, data consumers or both. This section looks at them as data producers, where they implement both the interfaces Iterable and Iterator (shown below). That means that the result of a generator function is both an iterable and an iterator. The full interface of generator objects will be shown later.
  1. interface Iterable {
  2. Symbol.iterator : Iterator;
  3. }
  4. interface Iterator {
  5. next() : IteratorResult;
  6. }
  7. interface IteratorResult {
  8. value : any;
  9. done : boolean;
  10. }
I have omitted method return() of interface Iterable, because it is not relevant in this section. A generator function produces a sequence of values via yield, a data consumer consumes thoses values via the iterator method next(). For example, the following generator function produces the values 'a' and 'b':
  1. function genFunc() {
  2. yield 'a';
  3. yield 'b';
  4. }
This interaction shows how to retrieve the yielded values via the generator object genObj:
  1. > const genObj = genFunc();
  2. > genObj.next()
  3. { value: 'a', done: false }
  4. > genObj.next()
  5. { value: 'b', done: false }
  6. > genObj.next() // done: true => end of sequence
  7. { value: undefined, done: true }
#### 22.3.1 Ways of iterating over a generator # As generator objects are iterable, ES6 language constructs that support iterables can be applied to them. The following three ones are especially important. First, the for-of loop:
  1. for (const x of genFunc()) {
  2. console.log(x);
  3. }
  4. // Output:
  5. // a
  6. // b
Second, the spread operator (), which turns iterated sequences into elements of an array (consult the chapter on parameter handling for more information on this operator):
  1. const arr = […genFunc()]; // ['a', 'b']
Third, destructuring:
  1. > const [x, y] = genFunc();
  2. > x
  3. 'a'
  4. > y
  5. 'b'
#### 22.3.2 Returning from a generator # The previous generator function did not contain an explicit return. An implicit return is equivalent to returning undefined. Let’s examine a generator with an explicit return:
  1. function genFuncWithReturn() {
  2. yield 'a';
  3. yield 'b';
  4. return 'result';
  5. }
The returned value shows up in the last object returned by next(), whose property done is true:
  1. > const genObjWithReturn = genFuncWithReturn();
  2. > genObjWithReturn.next()
  3. { value: 'a', done: false }
  4. > genObjWithReturn.next()
  5. { value: 'b', done: false }
  6. > genObjWithReturn.next()
  7. { value: 'result', done: true }
However, most constructs that work with iterables ignore the value inside the done object:
  1. for (const x of genFuncWithReturn()) {
  2. console.log(x);
  3. }
  4. // Output:
  5. // a
  6. // b
  7.  
  8. const arr = […genFuncWithReturn()]; // ['a', 'b']
yield, an operator for making recursive generator calls, does consider values inside done objects. It is explained later. #### 22.3.3 Throwing an exception from a generator # If an exception leaves the body of a generator then next() throws it:
  1. function genFunc() {
  2. throw new Error('Problem!');
  3. }
  4. const genObj = genFunc();
  5. genObj.next(); // Error: Problem!
That means that next() can produce three different “results”: - For an item x in an iteration sequence, it returns { value: x, done: false } - For the end of an iteration sequence with a return value z, it returns { value: z, done: true } - For an exception that leaves the generator body, it throws that exception. #### 22.3.4 Example: iterating over properties # Let’s look at an example that demonstrates how convenient generators are for implementing iterables. The following function, objectEntries(), returns an iterable over the properties of an object:
  1. function objectEntries(obj) {
  2. // In ES6, you can use strings or symbols as property keys,
  3. // Reflect.ownKeys() retrieves both
  4. const propKeys = Reflect.ownKeys(obj);
  5. for (const propKey of propKeys) {
  6. yield [propKey, obj[propKey]];
  7. }
  8. }
This function enables you to iterate over the properties of an object jane via the for-of loop:
  1. const jane = { first: 'Jane', last: 'Doe' };
  2. for (const [key,value] of objectEntries(jane)) {
  3. console.log(</code><code>${</code><code>key</code><code>}</code><code>: </code><code>${</code><code>value</code><code>}</code><code>);
  4. }
  5. // Output:
  6. // first: Jane
  7. // last: Doe
For comparison – an implementation of objectEntries() that doesn’t use generators is much more complicated:
  1. function objectEntries(obj) {
  2. let index = 0;
  3. let propKeys = Reflect.ownKeys(obj);
  4.  
  5. return {
  6. Symbol.iterator {
  7. return this;
  8. },
  9. next() {
  10. if (index < propKeys.length) {
  11. let key = propKeys[index];
  12. index++;
  13. return { value: [key, obj[key]] };
  14. } else {
  15. return { done: true };
  16. }
  17. }
  18. };
  19. }
#### 22.3.5 You can only yield in generators # A significant limitation of generators is that you can only yield while you are (statically) inside a generator function. That is, yielding in callbacks doesn’t work:
  1. function genFunc() {
  2. ['a', 'b'].forEach(x => yield x); // SyntaxError
  3. }
yield is not allowed inside non-generator functions, which is why the previous code causes a syntax error. In this case, it is easy to rewrite the code so that it doesn’t use callbacks (as shown below). But unfortunately that isn’t always possible.
  1. function genFunc() {
  2. for (const x of ['a', 'b']) {
  3. yield x; // OK
  4. }
  5. }
The upside of this limitation is explained later: it makes generators easier to implement and compatible with event loops. #### 22.3.6 Recursion via yield # You can only use yield within a generator function. Therefore, if you want to implement a recursive algorithm with generator, you need a way to call one generator from another one. This section shows that that is more complicated than it sounds, which is why ES6 has a special operator, yield, for this. For now, I only explain how yield works if both generators produce output, I’ll later explain how things work if input is involved. How can one generator recursively call another generator? Let’s assume you have written a generator function foo:
  1. function foo() {
  2. yield 'a';
  3. yield 'b';
  4. }
How would you call foo from another generator function bar? The following approach does not work!
  1. function bar() {
  2. yield 'x';
  3. foo(); // does nothing!
  4. yield 'y';
  5. }
Calling foo() returns an object, but does not actually execute foo(). That’s why ECMAScript 6 has the operator yield for making recursive generator calls:
  1. function bar() {
  2. yield 'x';
  3. yield foo();
  4. yield 'y';
  5. }
  6. // Collect all values yielded by bar() in an array
  7. const arr = […bar()];
  8. // ['x', 'a', 'b', 'y']
Internally, yield works roughly as follows:
  1. function bar() {
  2. yield 'x';
  3. for (const value of foo()) {
  4. yield value;
  5. }
  6. yield 'y';
  7. }
The operand of yield does not have to be a generator object, it can be any iterable:
  1. function bla() {
  2. yield 'sequence';
  3. yield ['of', 'yielded'];
  4. yield 'values';
  5. }
  6.  
  7. const arr = […bla()];
  8. // ['sequence', 'of', 'yielded', 'values']
##### 22.3.6.1 yield considers end-of-iteration values # Most constructs that support iterables ignore the value included in the end-of-iteration object (whose property done is true). Generators provide that value via return. The result of yield is the end-of-iteration value:
  1. function genFuncWithReturn() {
  2. yield 'a';
  3. yield 'b';
  4. return 'The result';
  5. }
  6. function logReturned(genObj) {
  7. const result = yield genObj;
  8. console.log(result); // (A)
  9. }
If we want to get to line A, we first must iterate over all values yielded by logReturned():
  1. > […logReturned(genFuncWithReturn())]
  2. The result
  3. [ 'a', 'b' ]
##### 22.3.6.2 Iterating over trees # Iterating over a tree with recursion is simple, writing an iterator for a tree with traditional means is complicated. That’s why generators shine here: they let you implement an iterator via recursion. As an example, consider the following data structure for binary trees. It is iterable, because it has a method whose key is Symbol.iterator. That method is a generator method and returns an iterator when called.
  1. class BinaryTree {
  2. constructor(value, left=null, right=null) {
  3. this.value = value;
  4. this.left = left;
  5. this.right = right;
  6. }
  7. /** Prefix iteration /
  8. Symbol.iterator {
  9. yield this.value;
  10. if (this.left) {
  11. yield this.left;
  12. // Short for: yield this.leftSymbol.iterator
  13. }
  14. if (this.right) {
  15. yield this.right;
  16. }
  17. }
  18. }
The following code creates a binary tree and iterates over it via for-of:
  1. const tree = new BinaryTree('a',
  2. new BinaryTree('b',
  3. new BinaryTree('c'),
  4. new BinaryTree('d')),
  5. new BinaryTree('e'));
  6.  
  7. for (const x of tree) {
  8. console.log(x);
  9. }
  10. // Output:
  11. // a
  12. // b
  13. // c
  14. // d
  15. // e
### 22.4 Generators as observers (data consumption) # As consumers of data, generator objects conform to the second half of the generator interface, Observer:
  1. interface Observer {
  2. next(value? : any) : void;
  3. return(value? : any) : void;
  4. throw(error) : void;
  5. }
As an observer, a generator pauses until it receives input. There are three kinds of input, transmitted via the methods specified by the interface: - next() sends normal input. - return() terminates the generator. - throw() signals an error. #### 22.4.1 Sending values via next() # If you use a generator as an observer, you send values to it via next() and it receives those values via yield:
  1. function dataConsumer() {
  2. console.log('Started');
  3. console.log(1. </code><code>${</code><code>yield</code><code>}</code><code>); // (A)
  4. console.log(2. </code><code>${</code><code>yield</code><code>}</code><code>);
  5. return 'result';
  6. }
Let’s use this generator interactively. First, we create a generator object:
  1. > const genObj = dataConsumer();
We now call genObj.next(), which starts the generator. Execution continues until the first yield, which is where the generator pauses. The result of next() is the value yielded in line A (undefined, because yield doesn’t have an operand). In this section, we are not interested in what next() returns, because we only use it to send values, not to retrieve values.
  1. > genObj.next()
  2. Started
  3. { value: undefined, done: false }
We call next() two more times, in order to send the value 'a' to the first yield and the value 'b' to the second yield:
  1. > genObj.next('a')
  2. 1. a
  3. { value: undefined, done: false }
  4.  
  5. > genObj.next('b')
  6. 2. b
  7. { value: 'result', done: true }
The result of the last next() is the value returned from dataConsumer(). done being true indicates that the generator is finished. Unfortunately, next() is asymmetric, but that can’t be helped: It always sends a value to the currently suspended yield, but returns the operand of the following yield. ##### 22.4.1.1 The first next() # When using a generator as an observer, it is important to note that the only purpose of the first invocation of next() is to start the observer. It is only ready for input afterwards, because this first invocation advances execution to the first yield. Therefore, any input you send via the first next() is ignored:
  1. function gen() {
  2. // (A)
  3. while (true) {
  4. const input = yield; // (B)
  5. console.log(input);
  6. }
  7. }
  8. const obj = gen();
  9. obj.next('a');
  10. obj.next('b');
  11.  
  12. // Output:
  13. // b
Initially, execution is paused in line A. The first invocation of next(): - Feeds the argument 'a' of next() to the generator, which has no way to receive it (as there is no yield). That’s why it is ignored. - Advances to the yield in line B and pauses execution. - Returns yield’s operand (undefined, because it doesn’t have an operand). The second invocation of next(): - Feeds the argument 'b' of next() to the generator, which receives it via the yield in line B and assigns it to the variable input. - Then execution continues until the next loop iteration, where it is paused again, in line B. - Then next() returns with that yield’s operand (undefined). The following utility function fixes this issue:
  1. /*
  2. Returns a function that, when called,
  3. returns a generator object that is immediately
  4. ready for input via next()
  5. /
  6. function coroutine(generatorFunction) {
  7. return function (…args) {
  8. const generatorObject = generatorFunction(…args);
  9. generatorObject.next();
  10. return generatorObject;
  11. };
  12. }
To see how coroutine() works, let’s compare a wrapped generator with a normal one:
  1. const wrapped = coroutine(function () {
  2. console.log(First input: </code><code>${</code><code>yield</code><code>}</code><code>);
  3. return 'DONE';
  4. });
  5. const normal = function () {
  6. console.log(First input: </code><code>${</code><code>yield</code><code>}</code><code>);
  7. return 'DONE';
  8. };
The wrapped generator is immediately ready for input:
  1. > wrapped().next('hello!')
  2. First input: hello!
The normal generator needs an extra next() until it is ready for input:
  1. > const genObj = normal();
  2. > genObj.next()
  3. { value: undefined, done: false }
  4. > genObj.next('hello!')
  5. First input: hello!
  6. { value: 'DONE', done: true }
#### 22.4.2 yield binds loosely # yield binds very loosely, so that we don’t have to put its operand in parentheses:
  1. yield a + b + c;
This is treated as:
  1. yield (a + b + c);
Not as:
  1. (yield a) + b + c;
As a consequence, many operators bind more tightly than yield and you have to put yield in parentheses if you want to use it as an operand. For example, you get a SyntaxError if you make an unparenthesized yield an operand of plus:
  1. console.log('Hello' + yield); // SyntaxError
  2. console.log('Hello' + yield 123); // SyntaxError
  3.  
  4. console.log('Hello' + (yield)); // OK
  5. console.log('Hello' + (yield 123)); // OK
You do not need parens if yield is a direct argument in a function or method call:
  1. foo(yield 'a', yield 'b');
You also don’t need parens if you use yield on the right-hand side of an assignment:
  1. const input = yield;
##### 22.4.2.1 yield in the ES6 grammar # The need for parens around yield can be seen in the following grammar rules in the ECMAScript 6 specification. These rules describe how expressions are parsed. I list them here from general (loose binding, lower precedence) to specific (tight binding, higher precedence). Wherever a certain kind of expression is demanded, you can also use more specific ones. The opposite is not true. The hierarchy ends with ParenthesizedExpression, which means that you can mention any expression anywhere, if you put it in parentheses.
  1. Expression :
  2. AssignmentExpression
  3. Expression , AssignmentExpression
  4. AssignmentExpression :
  5. ConditionalExpression
  6. YieldExpression
  7. ArrowFunction
  8. LeftHandSideExpression = AssignmentExpression
  9. LeftHandSideExpression AssignmentOperator AssignmentExpression
  10.  
  11. ···
  12.  
  13. AdditiveExpression :
  14. MultiplicativeExpression
  15. AdditiveExpression + MultiplicativeExpression
  16. AdditiveExpression - MultiplicativeExpression
  17. MultiplicativeExpression :
  18. UnaryExpression
  19. MultiplicativeExpression MultiplicativeOperator UnaryExpression
  20.  
  21. ···
  22.  
  23. PrimaryExpression :
  24. this
  25. IdentifierReference
  26. Literal
  27. ArrayLiteral
  28. ObjectLiteral
  29. FunctionExpression
  30. ClassExpression
  31. GeneratorExpression
  32. RegularExpressionLiteral
  33. TemplateLiteral
  34. ParenthesizedExpression
  35. ParenthesizedExpression :
  36. ( Expression )
The operands of an AdditiveExpression are an AdditiveExpression and a MultiplicativeExpression. Therefore, using a (more specific) ParenthesizedExpression as an operand is OK, but using a (more general) YieldExpression isn’t. #### 22.4.3 return() and throw() # Generator objects have two additional methods, return() and throw(), that are similar to next(). Let’s recap how next(x) works (after the first invocation): - The generator is currently suspended at a yield operator. - Send the value x to that yield, which means that it evaluates to x. - Proceed to the next yield, return or throw: - yield x leads to next() returning with { value: x, done: false } - return x leads to next() returning with { value: x, done: true } - throw err (not caught inside the generator) leads to next() throwing err. return() and throw() work similarly to next(), but they do something different in step 2: - return(x) executes return x at the location of yield. - throw(x) executes throw x at the location of yield. #### 22.4.4 return() terminates the generator # return() performs a return at the location of the yield that led to the last suspension of the generator. Let’s use the following generator function to see how that works.
  1. function genFunc1() {
  2. try {
  3. console.log('Started');
  4. yield; // (A)
  5. } finally {
  6. console.log('Exiting');
  7. }
  8. }
In the following interaction, we first use next() to start the generator and to proceed until the yield in line A. Then we return from that location via return().
  1. > const genObj1 = genFunc1();
  2. > genObj1.next()
  3. Started
  4. { value: undefined, done: false }
  5. > genObj1.return('Result')
  6. Exiting
  7. { value: 'Result', done: true }
##### 22.4.4.1 Preventing termination # You can prevent return() from terminating the generator if you yield inside the finally clause (using a return statement in that clause is also possible):
  1. function genFunc2() {
  2. try {
  3. console.log('Started');
  4. yield;
  5. } finally {
  6. yield 'Not done, yet!';
  7. }
  8. }
This time, return() does not exit the generator function. Accordingly, the property done of the object it returns is false.
  1. > const genObj2 = genFunc2();
  2.  
  3. > genObj2.next()
  4. Started
  5. { value: undefined, done: false }
  6.  
  7. > genObj2.return('Result')
  8. { value: 'Not done, yet!', done: false }
You can invoke next() one more time. Similarly to non-generator functions, the return value of the generator function is the value that was queued prior to entering the finally clause.
  1. > genObj2.next()
  2. { value: 'Result', done: true }
##### 22.4.4.2 Returning from a newborn generator # Returning a value from a newborn generator (that hasn’t started yet) is allowed:
  1. > function genFunc() {}
  2. > genFunc().return('yes')
  3. { value: 'yes', done: true }

22.4.5 throw() signals an error

throw() throws an exception at the location of the yield that led to the last suspension of the generator. Let’s examine how that works via the following generator function.

  1. function* genFunc1() {
  2. try {
  3. console.log('Started');
  4. yield; // (A)
  5. } catch (error) {
  6. console.log('Caught: ' + error);
  7. }
  8. }

In the following interaction, we first use next() to start the generator and proceed until the yield in line A. Then we throw an exception from that location.

  1. > const genObj1 = genFunc1();
  2.  
  3. > genObj1.next()
  4. Started
  5. { value: undefined, done: false }
  6.  
  7. > genObj1.throw(new Error('Problem!'))
  8. Caught: Error: Problem!
  9. { value: undefined, done: true }

The result of throw() (shown in the last line) stems from us leaving the function with an implicit return.

22.4.5.1 Throwing from a newborn generator

Throwing an exception in a newborn generator (that hasn’t started yet) is allowed:

  1. > function* genFunc() {}
  2. > genFunc().throw(new Error('Problem!'))
  3. Error: Problem!

22.4.6 Example: processing asynchronously pushed data

The fact that generators-as-observers pause while they wait for input makes them perfect for on-demand processing of data that is received asynchronously. The pattern for setting up a chain of generators for processing is as follows:

  • Each member of the chain of generators (except the last one) has a parameter target. It receives data via yield and sends data via target.next().
  • The last member of the chain of generators has no parameter target and only receives data. The whole chain is prefixed by a non-generator function that makes an asynchronous request and pushes the results into the chain of generators via next().

As an example, let’s chain generators to process a file that is read asynchronously.

The following code sets up the chain: it contains the generators splitLines, numberLines and printLines. Data is pushed into the chain via the non-generator function readFile.
  1. readFile(fileName, splitLines(numberLines(printLines())));
I’ll explain what these functions do when I show their code. As previously explained, if generators receive input via yield, the first invocation of next() on the generator object doesn’t do anything. That’s why I use the previously shown helper function coroutine() to create coroutines here. It executes the first next() for us. readFile() is the non-generator function that starts everything:
  1. import {createReadStream} from 'fs';
  2.  
  3. /
  4. Creates an asynchronous ReadStream for the file whose name
  5. is fileName and feeds it to the generator object target.
  6. @see ReadStream https://nodejs.org/api/fs.html#fs_class_fs_readstream
  7. */
  8. function readFile(fileName, target) {
  9. const readStream = createReadStream(fileName,
  10. { encoding: 'utf8', bufferSize: 1024 });
  11. readStream.on('data', buffer => {
  12. const str = buffer.toString('utf8');
  13. target.next(str);
  14. });
  15. readStream.on('end', () => {
  16. // Signal end of output sequence
  17. target.return();
  18. });
  19. }
The chain of generators starts with splitLines:
  1. /
  2. Turns a sequence of text chunks into a sequence of lines
  3. (where lines are separated by newlines)
  4. /
  5. const splitLines = coroutine(function (target) {
  6. let previous = '';
  7. try {
  8. while (true) {
  9. previous += yield;
  10. let eolIndex;
  11. while ((eolIndex = previous.indexOf('\n')) >= 0) {
  12. const line = previous.slice(0, eolIndex);
  13. target.next(line);
  14. previous = previous.slice(eolIndex+1);
  15. }
  16. }
  17. } finally {
  18. // Handle the end of the input sequence
  19. // (signaled via return())
  20. if (previous.length > 0) {
  21. target.next(previous);
  22. }
  23. // Signal end of output sequence
  24. target.return();
  25. }
  26. });
Note an important pattern: - readFile uses the generator object method return() to signal the end of the sequence of chunks that it sends. - readFile sends that signal while splitLines is waiting for input via yield, inside an infinite loop. return() breaks from that loop. - splitLines uses a finally clause to handle the end-of-sequence. The next generator is numberLines:
  1. //
  2. Prefixes numbers to a sequence of lines
  3. /
  4. const numberLines = coroutine(function* (target) {
  5. try {
  6. for (const lineNo = 0; ; lineNo++) {
  7. const line = yield;
  8. target.next(</code><code>${</code><code>lineNo</code><code>}</code><code>: </code><code>${</code><code>line</code><code>}</code><code>);
  9. }
  10. } finally {
  11. // Signal end of output sequence
  12. target.return();
  13. }
  14. });
The last generator is printLines:
  1. /
  2. Receives a sequence of lines (without newlines)
  3. and logs them (adding newlines).
  4. /
  5. const printLines = coroutine(function () {
  6. while (true) {
  7. const line = yield;
  8. console.log(line);
  9. }
  10. });
The neat thing about this code is that everything happens lazily (on demand): lines are split, numbered and printed as they arrive; we don’t have to wait for all of the text before we can start printing. #### 22.4.7 yield: the full story # As a rough rule of thumb, yield performs (the equivalent of) a function call from one generator (the caller) to another generator (the callee). So far, we have only seen one aspect of yield: it propagates yielded values from the callee to the caller. Now that we are interested in generators receiving input, another aspect becomes relevant: yield also forwards input received by the caller to the callee. In a way, the callee becomes the active generator and can be controlled via the caller’s generator object. ##### 22.4.7.1 Example: yield forwards next() # The following generator function caller() invokes the generator function callee() via yield.
  1. function callee() {
  2. console.log('callee: ' + (yield));
  3. }
  4. function caller() {
  5. while (true) {
  6. yield callee();
  7. }
  8. }
callee logs values received via next(), which allows us to check whether it receives the value 'a' and 'b' that we send to caller.
  1. > const callerObj = caller();
  2.  
  3. > callerObj.next() // start
  4. { value: undefined, done: false }
  5.  
  6. > callerObj.next('a')
  7. callee: a
  8. { value: undefined, done: false }
  9.  
  10. > callerObj.next('b')
  11. callee: b
  12. { value: undefined, done: false }
throw() and return() are forwarded in a similar manner. ##### 22.4.7.2 The semantics of yield expressed in JavaScript # I’ll explain the complete semantics of yield by showing how you’d implemented it in JavaScript. The following statement:
  1. let yieldStarResult = yield calleeFunc();
is roughly equivalent to:
  1. let yieldStarResult;
  2.  
  3. const calleeObj = calleeFunc();
  4. let prevReceived = undefined;
  5. while (true) {
  6. try {
  7. // Forward input previously received
  8. const {value,done} = calleeObj.next(prevReceived);
  9. if (done) {
  10. yieldStarResult = value;
  11. break;
  12. }
  13. prevReceived = yield value;
  14. } catch (e) {
  15. // Pretend return can be caught like an exception
  16. if (e instanceof Return) {
  17. // Forward input received via return()
  18. calleeObj.return(e.returnedValue);
  19. return e.returnedValue; // “re-throw”
  20. } else {
  21. // Forward input received via throw()
  22. calleeObj.throw(e); // may throw
  23. }
  24. }
  25. }
To keep things simple, several things are missing in this code: - The operand of yield
can be any iterable value. - return() and throw() are optional iterator methods. We should only call them if they exist. - If an exception is received and throw() does not exist, but return() does then return() is called (before throwing an exception) to give calleeObject the opportunity to clean up. - calleeObj can refuse to close, by returning an object whose property done is false. Then the caller also has to refuse to close and yield must continue to iterate. ### 22.5 Generators as coroutines (cooperative multitasking) # We have seen generators being used as either sources or sinks of data. For many applications, it’s good practice to strictly separate these two roles, because it keeps things simpler. This section describes the full generator interface (which combines both roles) and one use case where both roles are needed: cooperative multitasking, where tasks must be able to both send and receive information. #### 22.5.1 The full generator interface # The full interface of generator objects, Generator, handles both output and input:
  1. interface Generator {
  2. next(value? : any) : IteratorResult;
  3. throw(value? : any) : IteratorResult;
  4. return(value? : any) : IteratorResult;
  5. }
  6. interface IteratorResult {
  7. value : any;
  8. done : boolean;
  9. }
The interface Generator combines two interfaces that we have seen previously: Iterator for output and Observer for input.
  1. interface Iterator { // data producer
  2. next() : IteratorResult;
  3. return?(value? : any) : IteratorResult;
  4. }
  5.  
  6. interface Observer { // data consumer
  7. next(value? : any) : void;
  8. return(value? : any) : void;
  9. throw(error) : void;
  10. }
#### 22.5.2 Cooperative multitasking # Cooperative multitasking is an application of generators where we need them to handle both output and input. Before we get into how that works, let’s first review the current state of parallelism in JavaScript. JavaScript runs in a single process. There are two ways in which this limitation is being abolished: - Multiprocessing: Web Workers let you run JavaScript in multiple processes. Shared access to data is one of the biggest pitfalls of multiprocessing. Web Workers avoid it by not sharing any data. That is, if you want a Web Worker to have a piece of data, you must send it a copy or transfer your data to it (after which you can’t access it anymore). - Cooperative multitasking: There are various patterns and libraries that experiment with cooperative multitasking. Multiple tasks are run, but only one at a time. Each task must explicitly suspend itself, giving it full control over when a task switch happens. In these experiments, data is often shared between tasks. But due to explicit suspension, there are few risks. Two use cases benefit from cooperative multitasking, because they involve control flows that are mostly sequential, anyway, with occasional pauses: - Streams: A task sequentially processes a stream of data and pauses if there is no data available. - For binary streams, WHATWG is currently working on a standard proposal that is based on callbacks and Promises. - For streams of data, Communicating Sequential Processes (CSP) are an interesting solution. A generator-based CSP library is covered later in this chapter. - Asynchronous computations: A task blocks (pauses) until it receives the result of a long- running computation. - In JavaScript, Promises have become a popular way of handling asynchronous computations. Support for them is included in ES6. The next section explains how generators can make using Promises simpler. ##### 22.5.2.1 Simplifying asynchronous computations via generators # Several Promise-based libraries simplify asynchronous code via generators. Generators are ideal as clients of Promises, because they can be suspended until a result arrives. The following example demonstrates what that looks like if one uses the library co by T.J. Holowaychuk. We need two libraries (if we run Node.js code via babel-node):
  1. import fetch from 'isomorphic-fetch';
  2. const co = require('co');
co is the actual library for cooperative multitasking, isomorphic-fetch is a polyfill for the new Promise-based fetch API (a replacement of XMLHttpRequest; read “That’s so fetch!” by Jake Archibald for more information). fetch makes it easy to write a function getFile that returns the text of a file at a url via a Promise:
  1. function getFile(url) {
  2. return fetch(url)
  3. .then(request => request.text());
  4. }
We now have all the ingredients to use co. The following task reads the texts of two files, parses the JSON inside them and logs the result.
  1. co(function () {
  2. try {
  3. const [croftStr, bondStr] = yield Promise.all([ // (A)
  4. getFile('http://localhost:8000/croft.json&#39;),
  5. getFile('http://localhost:8000/bond.json&#39;),
  6. ]);
  7. const croftJson = JSON.parse(croftStr);
  8. const bondJson = JSON.parse(bondStr);
  9.  
  10. console.log(croftJson);
  11. console.log(bondJson);
  12. } catch (e) {
  13. console.log('Failure to read: ' + e);
  14. }
  15. });
Note how nicely synchronous this code looks, even though it makes an asynchronous call in line A. A generator-as-task makes an async call by yielding a Promise to the scheduler function co. The yielding pauses the generator. Once the Promise returns a result, the scheduler resumes the generator by passing it the result via next(). A simple version of co looks as follows.
  1. function co(genFunc) {
  2. const genObj = genFunc();
  3. step(genObj.next());
  4.  
  5. function step({value,done}) {
  6. if (!done) {
  7. // A Promise was yielded
  8. value
  9. .then(result => {
  10. step(genObj.next(result)); // (A)
  11. })
  12. .catch(error => {
  13. step(genObj.throw(error)); // (B)
  14. });
  15. }
  16. }
  17. }
I have ignored that next() (line A) and throw() (line B) may throw exceptions (whenever an exception escapes the body of the generator function). #### 22.5.3 The limitations of cooperative multitasking via generators # Coroutines are cooperatively multitasked tasks that have no limitations: Inside a coroutine, any function can suspend the whole coroutine (the function activation itself, the activation of the function’s caller, the caller’s caller, etc.). In contrast, you can only suspend a generator from directly within a generator and only the current function activation is suspended. Due to these limitations, generators are occasionally called shallow coroutines [3]. ##### 22.5.3.1 The benefits of the limitations of generators # The limitations of generators have two main benefits: - Generators are compatible with event loops, which provide simple cooperative multitasking in browsers. I’ll explain the details momentarily. - Generators are relatively easy to implement, because only a single function activation needs to be suspended and because browsers can continue to use event loops. JavaScript already has a very simple style of cooperative multitasking: the event loop, which schedules the execution of tasks in a queue. Each task is started by calling a function and finished once that function is finished. Events, setTimeout() and other mechanisms add tasks to the queue. This style of multitasking makes one important guarantee: run to completion; every function can rely on not being interrupted by another task until it is finished. Functions become transactions and can perform complete algorithms without anyone seeing the data they operate on in an intermediate state. Concurrent access to shared data makes multitasking complicated and is not allowed by JavaScript’s concurrency model. That’s why run to completion is a good thing. Alas, coroutines prevent run to completion, because any function could suspend its caller. For example, the following algorithm consists of multiple steps:
  1. step1(sharedData);
  2. step2(sharedData);
  3. lastStep(sharedData);
If step2 was to suspend the algorithm, other tasks could run before the last step of the algorithm is performed. Those tasks could contain other parts of the application which would see sharedData in an unfinished state. Generators preserve run to completion, they only suspend themselves and return to their caller. co and similar libraries give you most of the power of coroutines, without their disadvantages: - They provide schedulers for tasks defined via generators. - Tasks “are” generators and can thus be fully suspended. - A recursive (generator) function call is only suspendable if it is done via yield*. That gives callers control over suspension. ### 22.6 Examples of generators # This section gives several examples of what generators can be used for.

22.6.1 Implementing iterables via generators

In the chapter on iteration, I implemented several iterables “by hand”. In this section, I use generators, instead.

22.6.1.1 The iterable combinator take()

take() converts a (potentially infinite) sequence of iterated values into a sequence of length n:

  1. function* take(n, iterable) {
  2. for (const x of iterable) {
  3. if (n <= 0) return;
  4. n--;
  5. yield x;
  6. }
  7. }

The following is an example of using it:

  1. const arr = ['a', 'b', 'c', 'd'];
  2. for (const x of take(2, arr)) {
  3. console.log(x);
  4. }
  5. // Output:
  6. // a
  7. // b

An implementation of take() without generators is more complicated:

  1. function take(n, iterable) {
  2. const iter = iterable[Symbol.iterator]();
  3. return {
  4. [Symbol.iterator]() {
  5. return this;
  6. },
  7. next() {
  8. if (n > 0) {
  9. n--;
  10. return iter.next();
  11. } else {
  12. maybeCloseIterator(iter);
  13. return { done: true };
  14. }
  15. },
  16. return() {
  17. n = 0;
  18. maybeCloseIterator(iter);
  19. }
  20. };
  21. }
  22. function maybeCloseIterator(iterator) {
  23. if (typeof iterator.return === 'function') {
  24. iterator.return();
  25. }
  26. }

Note that the iterable combinator zip() does not profit much from being implemented via a generator, because multiple iterables are involved and for-of can’t be used.

22.6.1.2 Infinite iterables

naturalNumbers() returns an iterable over all natural numbers:

  1. function* naturalNumbers() {
  2. for (let n=0;; n++) {
  3. yield n;
  4. }
  5. }

This function is often used in conjunction with a combinator:

  1. for (const x of take(3, naturalNumbers())) {
  2. console.log(x);
  3. }
  4. // Output
  5. // 0
  6. // 1
  7. // 2

Here is the non-generator implementation, so you can compare:

  1. function naturalNumbers() {
  2. let n = 0;
  3. return {
  4. [Symbol.iterator]() {
  5. return this;
  6. },
  7. next() {
  8. return { value: n++ };
  9. }
  10. }
  11. }
22.6.1.3 Array-inspired iterable combinators: map, filter

Arrays can be transformed via the methods map and filter. Those methods can be generalized to have iterables as input and iterables as output.

22.6.1.3.1 A generalized map()

This is the generalized version of map:

  1. function* map(iterable, mapFunc) {
  2. for (const x of iterable) {
  3. yield mapFunc(x);
  4. }
  5. }

map() works with infinite iterables:

  1. > [...take(4, map(naturalNumbers(), x => x * x))]
  2. [ 0, 1, 4, 9 ]
22.6.1.3.2 A generalized filter()

This is the generalized version of filter:

  1. function* filter(iterable, filterFunc) {
  2. for (const x of iterable) {
  3. if (filterFunc(x)) {
  4. yield x;
  5. }
  6. }
  7. }

filter() works with infinite iterables:

  1. > [...take(4, filter(naturalNumbers(), x => (x % 2) === 0))]
  2. [ 0, 2, 4, 6 ]

22.6.2 Generators for lazy evaluation

The next two examples show how generators can be used to process a stream of characters.

  • The input is a stream of characters.
  • Step 1 – tokenizing (characters → words): The characters are grouped into words, strings that match the regular expression /^[A-Za-z0-9]+$/. Non-word characters are ignored, but they separate words. The input of this step is a stream of characters, the output a stream of words.
  • Step 2 – extracting numbers (words → numbers): only keep words that match the regular expression /^[0-9]+$/ and convert them to numbers.
  • Step 3 – adding numbers (numbers → numbers): for every number received, return the total received so far. The neat thing is that everything is computed lazily (incrementally and on demand): computation starts as soon as the first character arrives. For example, we don’t have to wait until we have all characters to get the first word.
22.6.2.1 Lazy pull (generators as iterators)

Lazy pull with generators works as follows. The three generators implementing steps 1–3 are chained as follows:

  1. addNumbers(extractNumbers(tokenize(CHARS)))

Each of the chain members pulls data from a source and yields a sequence of items. Processing starts with tokenize whose source is the string CHARS.

22.6.2.1.1 Step 1 – tokenizing

The following trick makes the code a bit simpler: the end-of-sequence iterator result (whose property done is false) is converted into the sentinel value END_OF_SEQUENCE.

  1. /**
  2. * Returns an iterable that transforms the input sequence
  3. * of characters into an output sequence of words.
  4. */
  5. function* tokenize(chars) {
  6. const iterator = chars[Symbol.iterator]();
  7. let ch;
  8. do {
  9. ch = getNextItem(iterator); // (A)
  10. if (isWordChar(ch)) {
  11. let word = '';
  12. do {
  13. word += ch;
  14. ch = getNextItem(iterator); // (B)
  15. } while (isWordChar(ch));
  16. yield word; // (C)
  17. }
  18. // Ignore all other characters
  19. } while (ch !== END_OF_SEQUENCE);
  20. }
  21. const END_OF_SEQUENCE = Symbol();
  22. function getNextItem(iterator) {
  23. const {value,done} = iterator.next();
  24. return done ? END_OF_SEQUENCE : value;
  25. }
  26. function isWordChar(ch) {
  27. return typeof ch === 'string' && /^[A-Za-z0-9]$/.test(ch);
  28. }

How is this generator lazy? When you ask it for a token via next(), it pulls its iterator (lines A and B) as often as needed to produce as token and then yields that token (line C). Then it pauses until it is again asked for a token. That means that tokenization starts as soon as the first characters are available, which is convenient for streams.

Let’s try out tokenization. Note that the spaces and the dot are non-words. They are ignored, but they separate words. We use the fact that strings are iterables over characters (Unicode code points). The result of tokenize() is an iterable over words, which we turn into an array via the spread operator ().

  1. > [...tokenize('2 apples and 5 oranges.')]
  2. [ '2', 'apples', 'and', '5', 'oranges' ]
22.6.2.1.2 Step 2 – extracting numbers

This step is relatively simple, we only yield words that contain nothing but digits, after converting them to numbers via Number().

  1. /**
  2. * Returns an iterable that filters the input sequence
  3. * of words and only yields those that are numbers.
  4. */
  5. function* extractNumbers(words) {
  6. for (const word of words) {
  7. if (/^[0-9]+$/.test(word)) {
  8. yield Number(word);
  9. }
  10. }
  11. }

You can again see the laziness: If you ask for a number via next(), you get one (via yield) as soon as one is encountered in words.

Let’s extract the numbers from an Array of words:

  1. > [...extractNumbers(['hello', '123', 'world', '45'])]
  2. [ 123, 45 ]

Note that strings are converted to numbers.

22.6.2.1.3 Step 3 – adding numbers
  1. /**
  2. * Returns an iterable that contains, for each number in
  3. * `numbers`, the total sum of numbers encountered so far.
  4. * For example: 7, 4, -1 --> 7, 11, 10
  5. */
  6. function* addNumbers(numbers) {
  7. let result = 0;
  8. for (const n of numbers) {
  9. result += n;
  10. yield result;
  11. }
  12. }

Let’s try a simple example:

  1. > [...addNumbers([5, -2, 12])]
  2. [ 5, 3, 15 ]
22.6.2.1.4 Pulling the output

On its own, the chain of generator doesn’t produce output. We need to actively pull the output via the spread operator:

  1. const CHARS = '2 apples and 5 oranges.';
  2. const CHAIN = addNumbers(extractNumbers(tokenize(CHARS)));
  3. console.log([...CHAIN]);
  4. // [ 2, 7 ]

The helper function logAndYield allows us to examine whether things are indeed computed lazily:

  1. function* logAndYield(iterable, prefix='') {
  2. for (const item of iterable) {
  3. console.log(prefix + item);
  4. yield item;
  5. }
  6. }
  7.  
  8. const CHAIN2 = logAndYield(addNumbers(extractNumbers(tokenize(logAndYield(CHA\
  9. RS)))), '-> ');
  10. [...CHAIN2];
  11.  
  12. // Output:
  13. // 2
  14. //
  15. // -> 2
  16. // a
  17. // p
  18. // p
  19. // l
  20. // e
  21. // s
  22. //
  23. // a
  24. // n
  25. // d
  26. //
  27. // 5
  28. //
  29. // -> 7
  30. // o
  31. // r
  32. // a
  33. // n
  34. // g
  35. // e
  36. // s
  37. // .

The output shows that addNumbers produces a result as soon as the characters '2' and ' ' are received.

22.6.2.2 Lazy push (generators as observables)

Not much work is needed to convert the previous pull-based algorithm into a push-based one. The steps are the same. But instead of finishing via pulling, we start via pushing.

As previously explained, if generators receive input via yield, the first invocation of next() on the generator object doesn’t do anything. That’s why I use the previously shown helper function coroutine() to create coroutines here. It executes the first next() for us.

The following function send() does the pushing.

  1. /**
  2. * Pushes the items of `iterable` into `sink`, a generator.
  3. * It uses the generator method `next()` to do so.
  4. */
  5. function send(iterable, sink) {
  6. for (const x of iterable) {
  7. sink.next(x);
  8. }
  9. sink.return(); // signal end of stream
  10. }

When a generator processes a stream, it needs to be aware of the end of the stream, so that it can clean up properly. For pull, we did this via a special end-of-stream sentinel. For push, the end-of-stream is signaled via return().

Let’s test send() via a generator that simply outputs everything it receives:

  1. /**
  2. * This generator logs everything that it receives via `next()`.
  3. */
  4. const logItems = coroutine(function* () {
  5. try {
  6. while (true) {
  7. const item = yield; // receive item via `next()`
  8. console.log(item);
  9. }
  10. } finally {
  11. console.log('DONE');
  12. }
  13. });

Let’s send logItems() three characters via a string (which is an iterable over Unicode code points).

  1. > send('abc', logItems());
  2. a
  3. b
  4. c
  5. DONE
22.6.2.2.1 Step 1 – tokenize

Note how this generator reacts to the end of the stream (as signaled via return()) in two finally clauses. We depend on return() being sent to either one of the two yields. Otherwise, the generator would never terminate, because the infinite loop starting in line A would never terminate.

  1. /**
  2. * Receives a sequence of characters (via the generator object
  3. * method `next()`), groups them into words and pushes them
  4. * into the generator `sink`.
  5. */
  6. const tokenize = coroutine(function* (sink) {
  7. try {
  8. while (true) { // (A)
  9. let ch = yield; // (B)
  10. if (isWordChar(ch)) {
  11. // A word has started
  12. let word = '';
  13. try {
  14. do {
  15. word += ch;
  16. ch = yield; // (C)
  17. } while (isWordChar(ch));
  18. } finally {
  19. // The word is finished.
  20. // We get here if
  21. // - the loop terminates normally
  22. // - the loop is terminated via `return()` in line C
  23. sink.next(word); // (D)
  24. }
  25. }
  26. // Ignore all other characters
  27. }
  28. } finally {
  29. // We only get here if the infinite loop is terminated
  30. // via `return()` (in line B or C).
  31. // Forward `return()` to `sink` so that it is also
  32. // aware of the end of stream.
  33. sink.return();
  34. }
  35. });
  36.  
  37. function isWordChar(ch) {
  38. return /^[A-Za-z0-9]$/.test(ch);
  39. }

This time, the laziness is driven by push: as soon as the generator has received enough characters for a word (in line C), it pushes the word into sink (line D). That is, the generator does not wait until it has received all characters.

tokenize() demonstrates that generators work well as implementations of linear state machines. In this case, the machine has two states: “inside a word” and “not inside a word”.

Let’s tokenize a string:

  1. > send('2 apples and 5 oranges.', tokenize(logItems()));
  2. 2
  3. apples
  4. and
  5. 5
  6. oranges
22.6.2.2.2 Step 2 – extract numbers

This step is straightforward.

  1. /**
  2. * Receives a sequence of strings (via the generator object
  3. * method `next()`) and pushes only those strings to the generator
  4. * `sink` that are “numbers” (consist only of decimal digits).
  5. */
  6. const extractNumbers = coroutine(function* (sink) {
  7. try {
  8. while (true) {
  9. const word = yield;
  10. if (/^[0-9]+$/.test(word)) {
  11. sink.next(Number(word));
  12. }
  13. }
  14. } finally {
  15. // Only reached via `return()`, forward.
  16. sink.return();
  17. }
  18. });

Things are again lazy: as soon as a number is encountered, it is pushed to sink.

Let’s extract the numbers from an Array of words:

  1. > send(['hello', '123', 'world', '45'], extractNumbers(logItems()));
  2. 123
  3. 45
  4. DONE

Note that the input is a sequence of strings, while the output is a sequence of numbers.

22.6.2.2.3 Step 3 – add numbers

This time, we react to the end of the stream by pushing a single value and then closing the sink.

  1. /**
  2. * Receives a sequence of numbers (via the generator object
  3. * method `next()`). For each number, it pushes the total sum
  4. * so far to the generator `sink`.
  5. */
  6. const addNumbers = coroutine(function* (sink) {
  7. let sum = 0;
  8. try {
  9. while (true) {
  10. sum += yield;
  11. sink.next(sum);
  12. }
  13. } finally {
  14. // We received an end-of-stream
  15. sink.return(); // signal end of stream
  16. }
  17. });

Let’s try out this generator:

  1. > send([5, -2, 12], addNumbers(logItems()));
  2. 5
  3. 3
  4. 15
  5. DONE
22.6.2.2.4 Pushing the input

The chain of generators starts with tokenize and ends with logItems, which logs everything it receives. We push a sequence of characters into the chain via send:

  1. const INPUT = '2 apples and 5 oranges.';
  2. const CHAIN = tokenize(extractNumbers(addNumbers(logItems())));
  3. send(INPUT, CHAIN);
  4.  
  5. // Output
  6. // 2
  7. // 7
  8. // DONE

The following code proves that processing really happens lazily:

  1. const CHAIN2 = tokenize(extractNumbers(addNumbers(logItems({ prefix: '-> ' })\
  2. )));
  3. send(INPUT, CHAIN2, { log: true });
  4.  
  5. // Output
  6. // 2
  7. //
  8. // -> 2
  9. // a
  10. // p
  11. // p
  12. // l
  13. // e
  14. // s
  15. //
  16. // a
  17. // n
  18. // d
  19. //
  20. // 5
  21. //
  22. // -> 7
  23. // o
  24. // r
  25. // a
  26. // n
  27. // g
  28. // e
  29. // s
  30. // .
  31. // DONE

The output shows that addNumbers produces a result as soon as the characters '2' and ' ' are pushed.

22.6.3 Cooperative multi-tasking via generators

22.6.3.1 Pausing long-running tasks

In this example, we create a counter that is displayed on a web page. We improve an initial version until we have a cooperatively multitasked version that doesn’t block the main thread and the user interface.

This is the part of the web page in which the counter should be displayed:

  1. <body>
  2. Counter: <span id="counter"></span>
  3. </body>

This function displays a counter that counts up forever5:

  1. function countUp(start = 0) {
  2. const counterSpan = document.querySelector('#counter');
  3. while (true) {
  4. counterSpan.textContent = String(start);
  5. start++;
  6. }
  7. }

If you ran this function, it would completely block the user interface thread in which it runs and its tab would become unresponsive.

Let’s implement the same functionality via a generator that periodically pauses via yield (a scheduling function for running this generator is shown later):

  1. function* countUp(start = 0) {
  2. const counterSpan = document.querySelector('#counter');
  3. while (true) {
  4. counterSpan.textContent = String(start);
  5. start++;
  6. yield; // pause
  7. }
  8. }

Let’s add one small improvement. We move the update of the user interface to another generator, displayCounter, which we call via yield*. As it is a generator, it can also take care of pausing.

  1. function* countUp(start = 0) {
  2. while (true) {
  3. start++;
  4. yield* displayCounter(start);
  5. }
  6. }
  7. function* displayCounter(counter) {
  8. const counterSpan = document.querySelector('#counter');
  9. counterSpan.textContent = String(counter);
  10. yield; // pause
  11. }

Lastly, this is a scheduling function that we can use to run countUp(). Each execution step of the generator is handled by a separate task, which is created via setTimeout(). That means that the user interface can schedule other tasks in between and will remain responsive.

  1. function run(generatorObject) {
  2. if (!generatorObject.next().done) {
  3. // Add a new task to the event queue
  4. setTimeout(function () {
  5. run(generatorObject);
  6. }, 1000);
  7. }
  8. }

With the help of run, we get a (nearly) infinite count-up that doesn’t block the user interface:

  1. run(countUp());
22.6.3.2 Cooperative multitasking with generators and Node.js-style callbacks

If you call a generator function (or method), it does not have access to its generator object; its this is the this it would have if it were a non-generator function. A work-around is to pass the generator object into the generator function via yield.

The following Node.js script uses this technique, but wraps the generator object in a callback (next, line A). It must be run via babel-node.

  1. import {readFile} from 'fs';
  2.  
  3. const fileNames = process.argv.slice(2);
  4.  
  5. run(function* () {
  6. const next = yield;
  7. for (const f of fileNames) {
  8. const contents = yield readFile(f, { encoding: 'utf8' }, next);
  9. console.log('##### ' + f);
  10. console.log(contents);
  11. }
  12. });

In line A, we get a callback that we can use with functions that follow Node.js callback conventions. The callback uses the generator object to wake up the generator, as you can see in the implementation of run():

  1. function run(generatorFunction) {
  2. const generatorObject = generatorFunction();
  3.  
  4. // Step 1: Proceed to first `yield`
  5. generatorObject.next();
  6.  
  7. // Step 2: Pass in a function that the generator can use as a callback
  8. function nextFunction(error, result) {
  9. if (error) {
  10. generatorObject.throw(error);
  11. } else {
  12. generatorObject.next(result);
  13. }
  14. }
  15. generatorObject.next(nextFunction);
  16.  
  17. // Subsequent invocations of `next()` are triggered by `nextFunction`
  18. }
22.6.3.3 Communicating Sequential Processes (CSP)

The library js-csp brings Communicating Sequential Processes (CSP) to JavaScript, a style of cooperative multitasking that is similar to ClojureScript’s core.async and Go’s goroutines. js-csp has two abstractions:

  • Processes: are cooperatively multitasked tasks and implemented by handing a generator function to the scheduling function go().
  • Channels: are queues for communication between processes. Channels are created by calling chan(). As an example, let’s use CSP to handle DOM events, in a manner reminiscent of Functional Reactive Programming. The following code uses the function listen() (which is shown later) to create a channel that outputs mousemove events. It then continuously retrieves the output via take, inside an infinite loop. Thanks to yield, the process blocks until the channel has output.
  1. import csp from 'js-csp';
  2.  
  3. csp.go(function* () {
  4. const element = document.querySelector('#uiElement1');
  5. const channel = listen(element, 'mousemove');
  6. while (true) {
  7. const event = yield csp.take(channel);
  8. const x = event.layerX || event.clientX;
  9. const y = event.layerY || event.clientY;
  10. element.textContent = `${x}, ${y}`;
  11. }
  12. });

listen() is implemented as follows.

  1. function listen(element, type) {
  2. const channel = csp.chan();
  3. element.addEventListener(type,
  4. event => {
  5. csp.putAsync(channel, event);
  6. });
  7. return channel;
  8. }

22.7 Inheritance within the iteration API (including generators)

This is a diagram of how various objects are connected in ECMAScript 6 (it is based on Allen Wirf-Brock’s diagram in the ECMAScript specification):

22. Generators - 图1

Legend:

  • The white (hollow) arrows express the has-prototype relationship (inheritance) between objects. In other words: a white arrow from x to y means that Object.getPrototypeOf(x) === y.
  • Parentheses indicate that an object exists, but is not accessible via a global variable.
  • An instanceof arrow from x to y means that x instanceof y.
    • Remember that o instanceof C is equivalent to C.prototype.isPrototypeOf(o).
  • A prototype arrow from x to y means that x.prototype === y.
  • The right column shows an instance with its prototypes, the middle column shows a function and its prototypes, the left column shows classes for functions (metafunctions, if you will), connected via a subclass-of relationship. The diagram reveals two interesting facts:

First, a generator function g works very much like a constructor (however, you can’t invoke it via new; that causes a TypeError): The generator objects it creates are instances of it, methods added to g.prototype become prototype methods, etc.:

  1. > function* g() {}
  2. > g.prototype.hello = function () { return 'hi!'};
  3. > const obj = g();
  4. > obj instanceof g
  5. true
  6. > obj.hello()
  7. 'hi!'

Second, if you want to make methods available for all generator objects, it’s best to add them to (Generator).prototype. One way of accessing that object is as follows:

  1. const Generator = Object.getPrototypeOf(function* () {});
  2. Generator.prototype.hello = function () { return 'hi!'};
  3. const generatorObject = (function* () {})();
  4. generatorObject.hello(); // 'hi!'

22.7.1 IteratorPrototype

There is no (Iterator) in the diagram, because no such object exists. But, given how instanceof works and because (IteratorPrototype) is a prototype of g1(), you could still say that g1() is an instance of Iterator.

All iterators in ES6 have (IteratorPrototype) in their prototype chain. That object is iterable, because it has the following method. Therefore, all ES6 iterators are iterable (as a consequence, you can apply for-of etc. to them).

  1. [Symbol.iterator]() {
  2. return this;
  3. }

The specification recommends to use the following code to access (IteratorPrototype):

  1. const proto = Object.getPrototypeOf.bind(Object);
  2. const IteratorPrototype = proto(proto([][Symbol.iterator]()));

You could also use:

  1. const IteratorPrototype = proto(proto(function* () {}.prototype));

Quoting the ECMAScript 6 specification:

ECMAScript code may also define objects that inherit from IteratorPrototype. The IteratorPrototype object provides a place where additional methods that are applicable to all iterator objects may be added.

IteratorPrototype will probably become directly accessible in an upcoming version of ECMAScript and contain tool methods such as map() and filter() (source).

22.7.2 The value of this in generators

A generator function combines two concerns:

  • It is a function that sets up and returns a generator object.
  • It contains the code that the generator object steps through. That’s why it’s not immediately obvious what the value of this should be inside a generator.

In function calls and method calls, this is what it would be if gen() wasn’t a generator function, but a normal function:

  1. function* gen() {
  2. 'use strict'; // just in case
  3. yield this;
  4. }
  5.  
  6. // Retrieve the yielded value via destructuring
  7. const [functionThis] = gen();
  8. console.log(functionThis); // undefined
  9.  
  10. const obj = { method: gen };
  11. const [methodThis] = obj.method();
  12. console.log(methodThis === obj); // true

If you access this in a generator that was invoked via new, you get a ReferenceError (source: ES6 spec):

  1. function* gen() {
  2. console.log(this); // ReferenceError
  3. }
  4. new gen();

A work-around is to wrap the generator in a normal function that hands the generator its generator object via next(). That means that the generator must use its first yield to retrieve its generator object:

  1. const generatorObject = yield;

22.8 Style consideration: whitespace before and after the asterisk

Reasonable – and legal – variations of formatting the asterisk are:

  • A space before and after it:function * foo(x, y) { ··· }
  • A space before it:function *foo(x, y) { ··· }
  • A space after it:function* foo(x, y) { ··· }
  • No whitespace before and after it:function*foo(x, y) { ··· } Let’s figure out which of these variations make sense for which constructs and why.

22.8.1 Generator function declarations and expressions

Here, the star is only used because generator (or something similar) isn’t available as a keyword. If it were, then a generator function declaration would look like this:

  1. generator foo(x, y) {
  2. ···
  3. }

Instead of generator, ECMAScript 6 marks the function keyword with an asterisk. Thus, function* can be seen as a synonym for generator, which suggests writing generator function declarations as follows.

  1. function* foo(x, y) {
  2. ···
  3. }

Anonymous generator function expressions would be formatted like this:

  1. const foo = function* (x, y) {
  2. ···
  3. }

22.8.2 Generator method definitions

When writing generator method definitions, I recommend to format the asterisk as follows.

  1. const obj = {
  2. * generatorMethod(x, y) {
  3. ···
  4. }
  5. };

There are three arguments in favor of writing a space after the asterisk.

First, the asterisk shouldn’t be part of the method name. On one hand, it isn’t part of the name of a generator function. On the other hand, the asterisk is only mentioned when defining a generator, not when using it.

Second, a generator method definition is an abbreviation for the following syntax. (To make my point, I’m redundantly giving the function expression a name, too.)

  1. const obj = {
  2. generatorMethod: function* generatorMethod(x, y) {
  3. ···
  4. }
  5. };

If method definitions are about omitting the function keyword then the asterisk should be followed by a space.

Third, generator method definitions are syntactically similar to getters and setters (which are already available in ECMAScript 5):

  1. const obj = {
  2. get foo() {
  3. ···
  4. }
  5. set foo(value) {
  6. ···
  7. }
  8. };

The keywords get and set can be seen as modifiers of a normal method definition. Arguably, an asterisk is also such a modifier.

22.8.3 Formatting recursive yield

The following is an example of a generator function yielding its own yielded values recursively:

  1. function* foo(x) {
  2. ···
  3. yield* foo(x - 1);
  4. ···
  5. }

The asterisk marks a different kind of yield operator, which is why the above way of writing it makes sense.

22.8.4 Documenting generator functions and methods

Kyle Simpson (@getify) proposed something interesting: Given that we often append parentheses when we write about functions and methods such as Math.max(), wouldn’t it make sense to prepend an asterisk when writing about generator functions and methods? For example: should we write *foo() to refer to the generator function in the previous subsection? Let me argue against that.

When it comes to writing a function that returns an iterable, a generator is only one of the several options. I think it is better to not give away this implementation detail via marked function names.

Furthermore, you don’t use the asterisk when calling a generator function, but you do use parentheses.

Lastly, the asterisk doesn’t provide useful information – yield* can also be used with functions that return an iterable. But it may make sense to mark the names of functions and methods (including generators) that return iterables. For example, via the suffix Iter.

22.9 FAQ: generators

22.9.1 Why use the keyword function* for generators and not generator?

Due to backward compatibility, using the keyword generator wasn’t an option. For example, the following code (a hypothetical ES6 anonymous generator expression) could be an ES5 function call followed by a code block.

  1. generator (a, b, c) {
  2. ···
  3. }

I find that the asterisk naming scheme extends nicely to yield*.

22.9.2 Is yield a keyword?

yield is only a reserved word in strict mode. A trick is used to bring it to ES6 sloppy mode: it becomes a contextual keyword, one that is only available inside generators.

22.10 Conclusion

I hope that this chapter convinced you that generators are a useful and versatile tool.

I like that generators let you implement cooperatively multitasked tasks that block while making asynchronous function calls. In my opinion that’s the right mental model for async calls. Hopefully, JavaScript goes further in this direction in the future.

22.11 Further reading

Sources of this chapter:

[1] “Async Generator Proposal” by Jafar Husain

[2] “A Curious Course on Coroutines and Concurrency” by David Beazley

[3] “Why coroutines won’t work on the web” by David Herman