Please support this book: buy it or donate

35. Synchronous generators (advanced)



35.1. What are synchronous generators?

Synchronous generators are special versions of function definitions and method definitions that always return synchronous iterables:

  1. // Generator function declaration
  2. function* genFunc1() { /*···*/ }
  3. // Generator function expression
  4. const genFunc2 = function* () { /*···*/ };
  5. // Generator method definition in an object literal
  6. const obj = {
  7. * generatorMethod() {
  8. // ···
  9. }
  10. };
  11. // Generator method definition in a class definition
  12. // (class declaration or class expression)
  13. class MyClass {
  14. * generatorMethod() {
  15. // ···
  16. }
  17. }

Asterisks (*) mark functions and methods as generators:

  • Functions: The pseudo-keyword function* is a combination of the keyword function and an asterisk.
  • Methods: The * is a modifier (similar to static and get).

35.1.1. Generator functions return iterables and fill them via yield

If you call a generator function, it returns an iterable (actually: an iterator that is also iterable). The generator fills that iterable via the yield operator:

  1. function* genFunc1() {
  2. yield 'a';
  3. yield 'b';
  4. }
  5. const iterable = genFunc1();
  6. // Convert the iterable to an Array, to check what’s inside:
  7. assert.deepEqual([...iterable], ['a', 'b']);
  8. // You can also use a for-of loop
  9. for (const x of genFunc1()) {
  10. console.log(x);
  11. }
  12. // Output:
  13. // 'a'
  14. // 'b'

35.1.2. yield pauses a generator function

So far, yield looks like a simple way of adding values to an iterable. However, it does much more than that – it also pauses and exits the generator function:

  • Like return, a yield exits the body of the function.
  • Unlike return, if you invoke the function again, execution resumes directly after the yield.
    Let’s examine what that means via the following generator function.
  1. let location = 0;
  2. function* genFunc2() {
  3. location = 1;
  4. yield 'a';
  5. location = 2;
  6. yield 'b';
  7. location = 3;
  8. }

The result of a generator function is called a generator object. It is more than just an iterable, but that is beyond the scope of this book (consult “Exploring ES6” if you are interested in further details).

In order to use genFunc2(), we must first create the generator object genObj. genFunc2() is now paused “before” its body.

  1. const genObj = genFunc2();
  2. // genFunc2() is now paused “before” its body:
  3. assert.equal(location, 0);

genObj implements the iteration protocol. Therefore, we control the execution of genFunc2() via genObj.next(). Calling that method, resumes the paused genFunc2() and executes it until there is a yield. Then execution pauses and .next() returns the operand of the yield:

  1. assert.deepEqual(
  2. genObj.next(), {value: 'a', done: false});
  3. // genFunc2() is now paused directly after the first `yield`:
  4. assert.equal(location, 1);

Note that the yielded value 'a' is wrapped in an object, which is how iterables always deliver their values.

We call genObj.next() again and execution continues where we previously paused. Once we encounter the second yield, genFunc2() is paused and .next() returns the yielded value 'b'.

  1. assert.deepEqual(
  2. genObj.next(), {value: 'b', done: false});
  3. // genFunc2() is now paused directly after the second `yield`:
  4. assert.equal(location, 2);

We call genObj.next() one more time and execution continues until it leaves the body of genFunc2():

  1. assert.deepEqual(
  2. genObj.next(), {value: undefined, done: true});
  3. // We have reached the end of genFunc2():
  4. assert.equal(location, 3);

This time, property .done of the result of .next() is true, which means that the iterable is finished.

35.1.3. Why does yield pause execution?

What are the benefits of yield pausing execution? Why doesn’t it simply work like the Array method .push() and fill the iterable with values – without pausing?

Due to pausing, generators provide many of the features of coroutines (think processes that are multitasked cooperatively). For example, when you ask for the next value of an iterable, that value is computed lazily (on demand). The following two generator functions demonstrate what that means.

  1. /**
  2. * Returns an iterable over lines
  3. */
  4. function* genLines() {
  5. yield 'A line';
  6. yield 'Another line';
  7. yield 'Last line';
  8. }
  9. /**
  10. * Input: iterable over lines
  11. * Output: iterable over numbered lines
  12. */
  13. function* numberLines(lineIterable) {
  14. let lineNumber = 1;
  15. for (const line of lineIterable) { // input
  16. yield lineNumber + ': ' + line; // output
  17. lineNumber++;
  18. }
  19. }

Note that the yield inside numberLines() appears inside a for-of loop. yield can be used inside loops, but not inside callbacks (more on that later).

Let’s combine both generators to produce the iterable numberedLines:

  1. const numberedLines = numberLines(genLines());
  2. assert.deepEqual(
  3. numberedLines.next(), {value: '1: A line', done: false});
  4. assert.deepEqual(
  5. numberedLines.next(), {value: '2: Another line', done: false});

Every time we ask numberedLines for another value via .next(), numberLines() only asks genLines() for a single line and numbers it. If genLines() were to synchronously read its lines from a large file, we would be able to retrieve the first numbered line as soon as it is read from the file. If yield didn’t pause, we’d have to wait until genLines() is completely finished with reading.

35.1.4. Example: Mapping over iterables

The following function mapIter() is similar to Array.from(), but it returns an iterable, not an Array and produces its results on demand.

  1. function* mapIter(iterable, func) {
  2. let index = 0;
  3. for (const x of iterable) {
  4. yield func(x, index);
  5. index++;
  6. }
  7. }
  8. const iterable = mapIter(['a', 'b'], x => x + x);
  9. assert.deepEqual([...iterable], ['aa', 'bb']);

35.2. Calling generators from generators (advanced)

35.2.1. Calling generators via yield*

yield only works directly inside generators – so far we haven’t seen a way of delegating yielding to another function or method.

Let’s first examine what does not work: In the following example, we’d like foo() to call bar(), so that the latter yields two values for the former. Alas, a naive approach fails:

  1. function* foo() {
  2. // Nothing happens if we call `bar()`:
  3. bar();
  4. }
  5. function* bar() {
  6. yield 'a';
  7. yield 'b';
  8. }
  9. assert.deepEqual(
  10. [...foo()], []);

Why doesn’t this work? The function call bar() returns an iterable, which we ignore.

What we want is for foo() to yield everything that is yielded by bar(). That’s what the yield* operator does:

  1. function* foo() {
  2. yield* bar();
  3. }
  4. function* bar() {
  5. yield 'a';
  6. yield 'b';
  7. }
  8. assert.deepEqual(
  9. [...foo()], ['a', 'b']);

In other words, the previous foo() is roughly equivalent to:

  1. function* foo() {
  2. for (const x of bar()) {
  3. yield x;
  4. }
  5. }

Note that yield* works with any iterable:

  1. function* gen() {
  2. yield* [1, 2];
  3. }
  4. assert.deepEqual(
  5. [...gen()], [1, 2]);

35.2.2. Example: Iterating over a tree

yield* lets us make recursive calls in generators, which is useful when iterating over recursive data structures such as trees. Take, for example, the following data structure for binary trees.

  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: parent before children */
  8. * [Symbol.iterator]() {
  9. yield this.value;
  10. if (this.left) {
  11. // Same as yield* this.left[Symbol.iterator]()
  12. yield* this.left;
  13. }
  14. if (this.right) {
  15. yield* this.right;
  16. }
  17. }
  18. }

Method Symbol.iterator adds support for the iteration protocol, which means that we can use a for-of loop to iterate over an instance of BinaryTree:

  1. const tree = new BinaryTree('a',
  2. new BinaryTree('b',
  3. new BinaryTree('c'),
  4. new BinaryTree('d')),
  5. new BinaryTree('e'));
  6. for (const x of tree) {
  7. console.log(x);
  8. }
  9. // Output:
  10. // 'a'
  11. // 'b'
  12. // 'c'
  13. // 'd'
  14. // 'e'

35.3. Example: Reusing loops

One important use case for generators is extracting and reusing loop functionality.

35.3.1. The loop to reuse

As an example, consider the following function that iterates over a tree of files and logs their paths (it uses the Node.js API for doing so):

  1. function logFiles(dir) {
  2. for (const fileName of fs.readdirSync(dir)) {
  3. const filePath = path.resolve(dir, fileName);
  4. console.log(filePath);
  5. const stats = fs.statSync(filePath);
  6. if (stats.isDirectory()) {
  7. logFiles(filePath); // recursive call
  8. }
  9. }
  10. }
  11. const rootDir = process.argv[2];
  12. logFiles(rootDir);

How can we reuse this loop, to do something other than logging paths?

35.3.2. Internal iteration (push)

One way of reusing iteration code is via internal iteration: Each iterated value is passsed to a callback (line A).

  1. function iterFiles(dir, callback) {
  2. for (const fileName of fs.readdirSync(dir)) {
  3. const filePath = path.resolve(dir, fileName);
  4. callback(filePath); // (A)
  5. const stats = fs.statSync(filePath);
  6. if (stats.isDirectory()) {
  7. iterFiles(filePath, callback);
  8. }
  9. }
  10. }
  11. const rootDir = process.argv[2];
  12. const paths = [];
  13. iterFiles(rootDir, p => paths.push(p));

35.3.3. External iteration (pull)

Another way of reusing iteration code is via external iteration: We can write a generator that yields all iterated values.

  1. function* iterFiles(dir) {
  2. for (const fileName of fs.readdirSync(dir)) {
  3. const filePath = path.resolve(dir, fileName);
  4. yield filePath; // (A)
  5. const stats = fs.statSync(filePath);
  6. if (stats.isDirectory()) {
  7. yield* iterFiles(filePath);
  8. }
  9. }
  10. }
  11. const rootDir = process.argv[2];
  12. const paths = [...iterFiles(rootDir)];

35.4. Advanced features of generators