Understand Javascript Generators

Javascript generators are under-appreciated, yet they are actually very useful and powerful, in this article, we will explore the basics and the power of javascript generators.

Introduction

Javascript Generator is a regular function that does return Generator object.

Javascript Generator works well and conforms with iterable protocol and the iterator protocol, which allows for the creation of data streams with ease.

Generator functions

Syntax

The generator function is identified by having the (asterisk/star *) character next to the function keyword.

It can be written as the following:

function* f(…){} 
or
function *f(…){}

Both syntaxes are correct.

But usually, the first syntax is preferred, as the asterisk/star * denotes that it’s a generator function, it describes the kind, not the name, so it should stick with the function keyword.


How do JS generators work?

To create a generator function, we need a special syntax construct: function*, so-called “generator function”.

It looks like the following:

function* alphabitGenerator() {
    yield 'a';
    yield 'b';
    yield 'c';
    return 'd';
}

The Generator constructor type isn’t available globally, it only can be returned from a generator function. As shown in the example below.

function* alphabitGenerator() {
    yield 'a';
    yield 'b';
    yield 'c';
    return 'd';
}

const generator = alphabitGenerator();

// call generator without parentheses 
// generator
// Expected output: generator {<suspended>}

Generator functions behave differently from regular functions, you can think of generators as start/stop functions. Meaning once the generator function is called it won’t execute its all code at once, rather it will execute until the nearest yield <value> statement. And the return type would be of Generator type.

The main methods of the javascript generator function are:

  • next(): The result of the next() method call is always an object with two properties:
    • value: the yielded/returned value.
    • donetrue if the function code has finished, otherwise false.

For example, calling the next method on the above example would result in the following for the first call, and the code execution will stop at line 3 ” yield 'b'

const generator = alphabitGenerator();

generator.next(); 
// Expected output: {value: 'a', done: false}

Calling generator.next() for the second time, the code execution will resume and the following output will result:

const generator = alphabitGenerator();

generator.next(); // {value: 'a', done: false}
generator.next(); // {value: 'b', done: false}

The same would happen when we call the function for the 3rd time:

const generator = alphabitGenerator();

generator.next(); // {value: 'a', done: false}
generator.next(); // {value: 'b', done: false}
generator.next(); // {value: 'c', done: false}

Calling it for the 4th time, the execution will reach the return statement and the done property value will be true

const generator = alphabitGenerator();

generator.next(); // {value: 'a', done: false}
generator.next(); // {value: 'b', done: false}
generator.next(); // {value: 'c', done: false}
generator.next(); // {value: 'd', done: true}

Now the generator is done. We should see it from done:true and process value:'d' as the final result.

Calling the generator.next() again and again, would always result with value=undefined and done=true.

To recap:

.next() advances.

yield pauses.

return stops.


Generators are also iterable

We can loop over the generator values using for...of loop.

function* alphabitGenerator() {
  yield "a";
  yield "b";
  yield "c";
  return "d";
}

const generator = alphabitGenerator();

for (let value of generator) {
  console.log(value.toUpperCase());
}
// Expected Output:
// A
// B
// C

Note that the D letter wasn’t returned/printed, this is because for...of ignores the last value when done=true.

So, if we want all results to be shown by for..of, we must return them with yield:

function* alphabitGenerator() {
  yield "a";
  yield "b";
  yield "c";
  yield "d";
}

const generator = alphabitGenerator();

for (let value of generator) {
  console.log(value.toUpperCase());
}
// Expected Output:
// A
// B
// C
// D

As well since the generators are iterable we can apply all iterable protocol related functionalities, such as the spread syntax ...

function* alphabitGenerator() {
  yield "a";
  yield "b";
  yield "c";
  yield "d";
}

const generator = alphabitGenerator();
console.log([...generator])

// Expected Output:
// (4) ['a', 'b', 'c', 'd']


Practical uses of generators

1- Custom iterables

For example, we can convert a custom object list/map into an iterable using generators, such as the following:

const letters = {
  letter1: "a",
  letter2: "b",
  letter3: "c",
  [Symbol.iterator]: function* () {
    let index = 1;
    for (index; index <= Object.keys(this).length; index++) {
      yield this[`letter${index}`];
    }
  },
};

for (let letter of letters) {
  console.log(letter.toUpperCase());
}
// Expected Output:
// A
// B
// C

In the example above we’ve added a custom iterable functionality to the object which allowed us to use for...of loop, to loop over the elements of the object and print them in a capital case.


Another example for custom iterables, let’s build a functionality that allows us to generate numbers in the range from-to.

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]: function*() {
    for(let value = this.from; value <= this.to; value++) {
      yield value;
    }
  }
};

console.log( [...range] ); // 1,2,3,4,5

In the above example, we’ve created an object with two properties from and to, then we’ve attached a custom iterable functionality to it, this functionality will run through a loop and would return numbers inclusive in the range from-to.

2- Async iteration with asyncIterator

The following example demonstrates how we can send an HTTP request using asyncIterator and paginate through the results easily using Javascript generators.

getStarShips = (endpoint) =>
    async function*() {
        let nextUrl = `https://swapi.dev/api/${endpoint}`;
        while (nextUrl) {
            const response = await fetch(nextUrl);
            const data = await response.json();
            nextUrl = data.next;
            yield* data.results;
        }
    }

const starWars = ({
    ships: {
        [Symbol.asyncIterator]: getSwapiPagerator("starships")
    }
});

for await (const ship of starWars.ships) {
    console.log(ship.name);
}

// Expected output
CR90 corvette
Star Destroyer
Death Star
Millennium Falcon
Y-wing
X-wing
TIE Advanced x1
Executor
.
.
etc.

Generator composition

Generator composition is a unique feature of generators that allows to transparently “embed/call” generators in each other.

For instance, we have a function that generates a set of random numbers:

function* generateRandoms(count) {
    for (let i = 1; i <= count; i++) yield Math.random() * i;
}

The above function would generate a random number on each next() call, until we get random numbers equal to the count parameter.

Now, let’s re-use the above function to generate a set of random numbers at once without the need to call next() each time

function* getSetOfRandomNumbers() {
    yield* generateRandoms(5)
}

let randoms = [];

for (let number of getSetOfRandomNumbers()) {
    randoms.push(number)
}

console.log(randoms);

// Expected output:
(5) [0.9618312667780231, 0.5782118321378436, 0.058907648425587045, 2.373007701444269, 4.299121086412223]

So we’ve created a new function getSetOfRandomNumbers that calls the generateRandoms function and waiting for all the yielded values to be done using yield*.

The yield* directive delegates the execution to another generator. As in the above example, this means the

yield* generateRandoms(5) will iterate over the generateRandoms generator and forward its yielded values to the outer function getSetOfRandomNumbers.


yield is a two-way street

The Javascript generator yield is a two-way street, in addition to returning the result to the outside scope, it also can pass the value inside the generator.

To do so, we should call generator.next(input), with an input argument. That argument becomes the result of yield.

For example:

function* question() {
    // Pass a question to the outer code and wait for an answer
    let result = yield "What's Your Country?";

    alert(result);
}

let generator = question();

alert(generator.next().value)

generator.next('US');
  • The first call generator.next() should be always made without an argument (the argument will be ignored if passed). It starts the execution and returns the result of the first yield "What's Your Country?". At this point, the generator pauses the execution while staying on the same line.
  • Then, the result of the yield is displayed in an alert box.
  • On generator.next('US'), the generator resumes, and US gets in as the result: let result = 'US'.

Generator return method

The generator return method finishes the execution of the generator.

function* alphabitGenerator() {
  yield "a";
  yield "b";
}

const g = alphabitGenerator();

console.log(g.next()); // { value: 'a', done: false }
console.log(g.return('c')); // { value: 'c', done: true }
console.log(g.next()); // { value: undefined, done: true }

As shown above, once the return method is called, the generator finishes the execution, and if we tried to call the return method again on a completed generator, it will return that same value again.

A good use case of the generator return method is to stop the generator execution based on a specific condition.


Generator throw method

The generator throw method works the same as the generator return method, it acts as if an error is inserted inside the generator body.

For example:

function* question() {
    try {
        // Pass a question to the outer code and wait for an answer
        let result = yield "What's Your Country?"; // 2

        // The execution won't reach here, because the exception is thrown above
        alert(result); // 3
    } catch (e) {
        // This will execute
        alert(e); // 4
    }
}

let generator = question();

alert(generator.next().value)

generator.throw(new Error("Please enter a valid country value")); // 1

In the example above, line (3) won’t be executed, this is because we’ve thrown an exception at line (1) which leads to an exception at the line (2), and line (4) will show the error.


That’s it for Javascript Generators.

Photo from unsplash.

Related Posts

How to Capture Screenshots with Puppeteer In NodeJS

How to Capture Screenshots with Puppeteer In NodeJS

To Capture Screenshots with Puppeteer: Launch a Browser Instance Navigate to the Web Page Capture the Screenshot Introduction: Puppeteer is a powerful Node.js library that allows developers…

How to Minimize Puppeteer Browser Window To Tray

How to Minimize Puppeteer Browser Window To Tray

Puppeteer is a powerful tool for automating tasks in headless or non-headless web browsers using JavaScript. While Puppeteer is often used to perform actions within a browser,…

Intercepting Responses in Node.js with Puppeteer

Intercepting Responses in Node.js with Puppeteer

Introduction: Puppeteer is a powerful Node.js library that provides a high-level API for controlling headless Chrome or Chromium browsers. It’s widely used for web scraping, automated testing,…

Mastering React Component Re-rendering in Jest

Mastering React Component Re-rendering in Jest

In this hands-on guide, we’ll explore the art of optimizing React component re-rendering within Jest tests. By combining theory with practical coding examples, you’ll gain a deep…

Eliminating Nesting Loops in React Rendering

Eliminating Nesting Loops in React Rendering

React has ushered in a new era of web application development with its component-based structure, promoting code reusability and maintainability. But as projects evolve, achieving optimal performance…

Exploring Type and Interface Usage in TypeScript

Exploring Type and Interface Usage in TypeScript

TypeScript has gained immense popularity by bridging the gap between dynamic JavaScript and static typing. Two of its fundamental features, “Type” and “Interface,” play pivotal roles in…

Leave a Reply

%d bloggers like this: