Synchronous generators Flashcards
What are synchronous generators?
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 keywordfunction
and an asterisk. - Methods: The
*
is a modifier (similar to static and get).
“What are synchronous generators?” (exploringjs.com). Retrieved October 1, 2024.
How are the iterables returned by generators fill?
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
“Generator functions return iterables and fill them via yield” (exploringjs.com). Retrieved October 1, 2024.
How does yield
work?
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
.
“yield
pauses a generator function” (exploringjs.com). Retrieved October 2, 2024.
Why does yield
pause execution?
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.
“Why does yield
pause execution?” (exploringjs.com). Retrieved October 2, 2024.
How can you call a generator from a generator?
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]
);
~~~
“Calling generators from generators (advanced)” (exploringjs.com). Retrieved October 3, 2024.