Synchronous generators Flashcards

1
Q

What are synchronous generators?

A

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

// Generator function declaration
function* genFunc1() { /*···*/ }

// Generator function expression
const genFunc2 = function* () { /*···*/ };

// Generator method definition in an object literal
const obj = {
  * generatorMethod() {
    // ···
  }
};

// Generator method definition in a class definition
// (class declaration or class expression)
class MyClass {
  * generatorMethod() {
    // ···
  }
}

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).
How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

How are the iterables returned by generators fill?

A

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

function* genFunc1() {
  yield 'a';
  yield 'b';
}

const iterable = genFunc1();
// Convert the iterable to an Array, to check what’s inside:
assert.deepEqual(
  Array.from(iterable), ['a', 'b']
);

// We can also use a for-of loop
for (const x of genFunc1()) {
  console.log(x);
}

Output:

a
b

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

How does yield work?

A

Using a generator function involves the following steps:

1.- Function-calling it returns an iterator iter (that is also an iterable).
2.- Iterating over iter repeatedly invokes iter.next(). Each time, we jump into the body of the generator function until there is a yield that returns a value.

Therefore, yield does more than just add values to iterables – it also pauses and exits the generator function:

Like return, a yield exits the body of the function and returns a value (to/via .next()).
Unlike return, if we repeat the invocation (of .next()), execution resumes directly after the yield.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

Why does yield pause execution?

A

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

```javascript
/**
* Returns an iterable over lines
/
function
genLines() {
yield ‘A line’;
yield ‘Another line’;
yield ‘Last line’;
}

/**
* Input: iterable over lines
* Output: iterable over numbered lines
/
function
numberLines(lineIterable) {
let lineNumber = 1;
for (const line of lineIterable) { // input
yield lineNumber + ‘: ‘ + line; // output
lineNumber++;
}
}
~~~

Note that the yield in 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:

```javascript
const numberedLines = numberLines(genLines());
assert.deepEqual(
numberedLines.next(), {value: ‘1: A line’, done: false});
assert.deepEqual(
numberedLines.next(), {value: ‘2: Another line’, done: false});
~~~

The key benefit of using generators here is that everything works incrementally: via numberedLines.next(), we ask numberLines() for only a single numbered line. In turn, it asks genLines() for only a single unnumbered line.

This incrementalism continues to work if, for example, genLines() reads its lines from a large text file: If we ask numberLines() for a numbered line, we get one as soon as genLines() has read its first line from the text file.

Without generators, genLines() would first read all lines and return them. Then numberLines() would number all lines and return them. We therefore have to wait much longer until we get the first numbered line.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How can you call a generator from a generator?

A

Via the yield* operator.

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:

```javascript
function* bar() {
yield ‘a’;
yield ‘b’;
}
function* foo() {
// Nothing happens if we call bar():
bar();
}
assert.deepEqual(
Array.from(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:

```javascript
function* bar() {
yield ‘a’;
yield ‘b’;
}
function* foo() {
yield* bar();
}
assert.deepEqual(
Array.from(foo()), [‘a’, ‘b’]
);
~~~

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

```javascript
function* foo() {
for (const x of bar()) {
yield x;
}
}
~~~

Note that yield* works with any iterable:

```javascript
function* gen() {
yield* [1, 2];
}
assert.deepEqual(
Array.from(gen()), [1, 2]
);
~~~

How well did you know this?
1
Not at all
2
3
4
5
Perfectly