JavaScript Generators
In this chapter, we are going to observe the JavaScript generators. Generators are functions that you can use for controlling the iterator. In comparison with regular functions that return a single value or nothing, generators return multiple values in a sequence, one after another. They operate great with iterables and allow creating data streams with ease.
Generator Functions¶
For creating a generator, it is necessary to use a particular syntax construct: function*, known as a generator function.
Here is a case in point:
function* generate() {
yield 1;
yield 2;
return 3;
}
Generator functions are different from regular ones. Every time a function like that is called, it doesn’t run its code. Alternatively, it returns a particular object, called “generator object” for managing the execution.
For instance:
function* generate() {
yield 1;
yield 2;
return 3;
}
// "generator function" creates "generator object"
let generator = generate();
console.log(generator); // [object Generator]
The primary method of a generator is next(). When you call it, it runs the execution till the nearest yield <value> statement. Afterward, the function execution suspends, and the yielded value on its turn is returned to the outer code.
The next()’ result is always an object with the following two properties:
- the value: that is the yielded value;
- done: true in case the function code has finished; otherwise, it’s false.
In the example below, the generator is created, getting its first yielded value:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
As of now, the first value is received, and the function execution is demonstrated on the second line.
Now, let’s try to call the generator.next() once more. It restores the code execution, returning the next yield, like this:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
let twoValue = generator.next();
console.log(JSON.stringify(twoValue)); // {value: 2, done: false}
In case you call it the third time, the execution will reach the return statement, which ends the function like this:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
let twoValue = generator.next();
console.log(JSON.stringify(twoValue)); // {value: 2, done: false}
let threeValue = generator.next();
console.log(JSON.stringify(threeValue)); // {value: 3, done: true}
Now, the generator is considered done. You can see it from done:true and process value:3 as the culminating result. Further calls to generator.next() will make no sense. If you decide to do them, they will return the same object {done: true}.
Now, let’s review the following syntaxes: function* f(…) and function *f(…). Both of these syntaxes are correct. But, as a rule, the first one is more preferable in the sense the * indicates that it’s a generator function, it outlines the kind, not the name, so it needs to stick with the function keyword.
Generators are Iterable¶
Looking at the done:true method, you may probably guess that generators are iterable.
It is possible to loop over their values using for..of, like here:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
for (let value of generator) {
console.log(value); // 1, then 2
}
Perhaps, you will agree that it is much more helpful than calling .next().value.
But, please, take into consideration that the example above shows 1, then 2, but it doesn’t show 3.
The reason is that for..of iteration ignores the last value when done: true. Hence, if you wish all the results to be demonstrated by for..of, you should return them by yield, like this:
function* generate() {
yield 1;
yield 2;
yield 3;
}
let generator = generate();
for (let value of generator) {
console.log(value); // 1, then 2, then 3
}
As generators are iterable, it is possible to call all related functionality. In other words, the spread syntax ….
Let’s take a look at an example:
function* generate() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generate()];
console.log(sequence); // 0, 1, 2, 3
Generator Composition¶
A unique feature of generators that allows embedding generators in each other transparently is called generator composition.
Let’s consider a function, which generates a sequence of numbers:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
There is also the option of reusing it to generate a more complicated order, like here:
- First, digits 0..9 (the character codes are 48…57).
- Followed by uppercase letters A..Z ( the character codes are 65…90).
- Followed by lowercase letters (the character codes 97…122).
the sequence is very convenient for creating passwords by selecting characters from it.
Within a regular function, to mix results from multiple other functions, you can call them, store the results, and join at the end.
Generators have a unique yield* syntax for composing one generator into another.
The composed generator will look as follows:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateCodes() {
// 0..9
yield* generate(48, 57);
// A..Z
yield* generate(65, 90);
// a..z
yield* generate(97, 122);
}
let str = '';
for (let code of generateCodes()) {
str += String.fromCharCode(code);
}
console.log(str); // 0..9A..Za..z
So, the execution is delegated to another generator by the yield* directive.
This term signifies that yield* gen iterates over the generator gen and forwards its yields outside transparently. Just as though the outer generator yielded the values.
So, the result will be as follows:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateCodes() {
// 0..9
yield* generate(48, 57);
// A..Z
yield* generate(65, 90);
// a..z
yield* generate(97, 122);
}
let str = '';
for (let code of generateCodes()) {
str += String.fromCharCode(code);
}
console.log(str); // 0..9A..Za..z
It can be assumed that a generator composition is a natural way of inserting a flow of one generator into another. No supplementary memory is used to store intermediate results.
“Yield” is a Two-way Street¶
As it was already stated, generators are like iterable objects with a unique syntax for generating values. But, they are even more flexible and powerful.
So, yield is considered a two-way street: it can both return the result to the outside and pass the value inside the generator.
To implement that, you need to call the generator.next(arg) using an argument. The argument will become the result of the yield.
For instance:
function* gen() {
// Pass a question to external code and wait for an answer
let result = yield "2 * 2 = ?"; // (*)
console.log(result);
}
let generator = gen();
let question = generator.next().value; // yield returns the value
generator.next(4); // pass the result into the generator
- The first generator.next() is always made without using any argument ( if an argument is passed, it is ignored). It begins the execution, returning the result of the first yield yield "2*2=?". At this stage, the generator suspends the execution, staying on the line (*).
- Afterward, the result of the yield gets into the question variable inside the calling call.
- On the generator.next(4), the generator restarts and 4 gets in as aresult, like this:
let result = 4
Please, take into consideration that the outer code doesn’t have to call next(4) immediately. It usually takes time. The generator, on its turn, waits.
Take a look at this example:
// resume the generator after some time
setTimeout(() => generator.next(4), 2000);
In contrast with the regular functions, a generator and the calling code can change the results passing values in next/yield.
Let’s see more calls in another example:
function* gen() {
let ask1 = yield "2 * 2 = ?";
console.log(ask1); // 4
let ask2 = yield "3 + 3 = ?";
console.log(ask2); // 6
}
let generator = gen();
console.log(generator.next().value); // "2 * 2 = ?"
console.log(generator.next(4).value); // "3 + 3 = ?"
console.log(generator.next(6).done); // true
- The initial .next() starts the execution.It reaches the first yield.
- The outcome is returned to the outer code.
- The 2nd .next(4) passes 4 back to the generator just as the result of the initial yield, resuming the execution.
- Then it gets to the second yield, which becomes the result of the generator code.
- The 3rd next(6) passes 6 into the generator just as the result of the second yield resuming the execution that reaches the end of the function:done: true. Every next(value) (except the first one) passes a value into the generator, which becomes the result of the current yield, then gets back the result of the next yield.
Generator.throw¶
As described above, the outer code can pass a value into the generator, as the result of yield.
To make things more obvious, let’s consider the yield of "2 * 2 = ?", which brings to an error:
function* gen() {
try {
let result = yield "2 * 2 = ?"; // (1)
console.log("The exception is thrown above, so the execution does not reach here");
} catch (e) {
console.log(e); // shows the error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer isn't found in database")); // (2)
The error, which is thrown into the generator line (2), leads to an exception in line (1) with yield. The try..catch catches and shows it, in the example above.
In case it is not caught, it will fall out the generator into the calling code.
The calling code’s current line is the line with generator.throw, labeled as (2). So you can catch it here, as follows:
function* generate() {
let result = yield "2 * 2 = ?"; // Error in this line
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer isn't found in database"));
} catch (err) {
console.log(err); // shows the error
}
If the error is not caught there, as a rule, it falls through to the outer calling code (if any), and if uncaught, destroys the script.
Summary¶
You can create generators with the help of generator functionsfunction* f(…) {…}. There is a unique yield operator inside generators.
The outer code and the generator can exchange results through next/yield calls.
Generators are not frequently used in JavaScript. But sometimes they are convenient, as the ability of a function to exchange data with the calling code during the execution is unique. And, finally, they work perfectly for making iterable objects.
0 Comments
CAN FEEDBACK
Emoji