Generators Flashcards
What are generators?
You can think of generators as processes (pieces of code) that you can pause and resume:
function* genFunc() { // (A) console.log('First'); yield; console.log('Second'); }
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:
const genObj = genFunc();
The process is initially paused in line A. genObj.next()
resumes execution, a yield
inside genFunc()
pauses execution:
genObj.next(); // Output: First genObj.next(); // output: Second
” What are generators? “ (exploringjs.com). Retrieved October 4, 2024.
What kind of generators are there?
There are four kinds of generators:
1.- Generator function declarations:
function* genFunc() { ··· } const genObj = genFunc();
2.- Generator function expressions:
const genFunc = function* () { ··· }; const genObj = genFunc();
3.- Generator method definitions in object literals:
const obj = { * generatorMethod() { ··· } }; const genObj = obj.generatorMethod();
4.- Generator method definitions in class definitions (class declarations or class expressions):
class MyClass { * generatorMethod() { ··· } } const myInst = new MyClass(); const genObj = myInst.generatorMethod();
” Kinds of generators” (exploringjs.com). Retrieved October 7, 2024.
How can you implement an iterable with a generator?
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:
function* objectEntries(obj) { const propKeys = Reflect.ownKeys(obj); for (const propKey of propKeys) { // `yield` returns a value and then pauses // the generator. Later, execution continues // where it was previously paused. yield [propKey, obj[propKey]]; } }
objectEntries()
is used like this:
const jane = { first: 'Jane', last: 'Doe' }; for (const [key,value] of objectEntries(jane)) { console.log(`${key}: ${value}`); } // Output: // first: Jane // last: Doe
“Use case: implementing iterables” (exploringjs.com). Retrieved October 7, 2024.
What roles can in generator play?
Generators can play three roles:
-
Iterators (data producers): Each
yield
can return a value vianext()
, which means that generators can produce sequences of values via loops and recursion. Due to generator objects implementing the interface Iterable, 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 fromnext()
(via a parameter). That means that generators become data consumers that pause until a new value is pushed into them vianext()
. - 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).
“Roles played by generators #” (exploringjs.com). Retrieved October 10, 2024.
Explain how a generator can be a data producer (iterable).
Generator objects can be data producers. This card shows an expample where an generator implements 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.
interface Iterable { [Symbol.iterator]() : Iterator; } interface Iterator { next() : IteratorResult; } interface IteratorResult { value : any; done : boolean; }
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'
:
function* genFunc() { yield 'a'; yield 'b'; }
This interaction shows how to retrieve the yielded values via the generator object genObj
:
> const genObj = genFunc(); > genObj.next() { value: 'a', done: false } > genObj.next() { value: 'b', done: false } > genObj.next() // done: true => end of sequence { value: undefined, done: true }
” Generators as iterators (data production) #” (exploringjs.com). Retrieved October 10, 2024.
Can a generator return a value?
Yes. All generators have a return
value. If no return
is explicitly set then it has an implicit return
which is equivalent to returning undefined
. Let’s examine a generator with an explicit return:
function* genFuncWithReturn() { yield 'a'; yield 'b'; return 'result'; }
The returned value shows up in the last object returned by next()
, whose property done
is true
:
> const genObjWithReturn = genFuncWithReturn(); > genObjWithReturn.next() { value: 'a', done: false } > genObjWithReturn.next() { value: 'b', done: false } > genObjWithReturn.next() { value: 'result', done: true }
However, most constructs that work with iterables ignore the value inside the done
object:
for (const x of genFuncWithReturn()) { console.log(x); } // Output: // a // b const arr = [...genFuncWithReturn()]; // ['a', 'b']
yield*
, an operator for making recursive generator calls, does consider values inside done
objects.
” Returning from a generator #” (exploringjs.com). Retrieved October 10, 2024.
can a generator throw an exception?
Yes, If an exception leaves the body of a generator then next()
throws it:
function* genFunc() { throw new Error('Problem!'); } const genObj = genFunc(); 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.
“Throwing an exception from a generator #” (exploringjs.com). Retrieved October 14, 2024.
Can you yield
outside of a generator?
No, 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:
function* genFunc() { ['a', 'b'].forEach(x => yield x); // SyntaxError }
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.
function* genFunc() { for (const x of ['a', 'b']) { yield x; // OK } }
“You can only yield in generators #” (exploringjs.com). Retrieved October 14, 2024.
How can you call a generator from within a generator?
With yield*
.
Directily calling the generator from within a generator does not work!
~~~
function* foo() {
yield ‘a’;
yield ‘b’;
}
function* bar() {
yield ‘x’;
foo(); // does nothing!
yield ‘y’;
}
~~~
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:
function* bar() { yield 'x'; yield* foo(); yield 'y'; } // Collect all values yielded by bar() in an array const arr = [...bar()]; // ['x', 'a', 'b', 'y'] Internally, yield* works roughly as follows: function* bar() { yield 'x'; for (const value of foo()) { yield value; } yield 'y'; }
The operand of yield*
does not have to be a generator object, it can be any iterable:
function* bla() { yield 'sequence'; yield* ['of', 'yielded']; yield 'values'; } const arr = [...bla()]; // ['sequence', 'of', 'yielded', 'values']
“Recursion via yield*
#” (exploringjs.com). Retrieved October 14, 2024.
What is the result of yield*
?
The result of yield*
is the end-of-iteration value:
function* genFuncWithReturn() { yield 'a'; yield 'b'; return 'The result'; } function* logReturned(genObj) { const result = yield* genObj; console.log(result); // (A) }
If we want to get to line A, we first must iterate over all values yielded by logReturned()
:
> [...logReturned(genFuncWithReturn())] The result [ 'a', 'b' ]
“yield*
considers end-of-iteration values #” (exploringjs.com). Retrieved October 18, 2024.
How can you use a generator to iterate over a tree?
Generators shine on tree iteration: 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.
```javascript
class BinaryTree {
constructor(value, left=null, right=null) {
this.value = value;
this.left = left;
this.right = right;
}
/** Prefix iteration */ * [Symbol.iterator]() { yield this.value; if (this.left) { yield* this.left; // Short for: yield* this.left[Symbol.iterator]() } if (this.right) { yield* this.right; } } } ~~~
The following code creates a binary tree and iterates over it via for-of
:
const tree = new BinaryTree('a', new BinaryTree('b', new BinaryTree('c'), new BinaryTree('d')), new BinaryTree('e')); for (const x of tree) { console.log(x); } // Output: // a // b // c // d // e
“Iterating over trees #” (exploringjs.com). Retrieved October 18, 2024.