JavaScript - generators

revision:


Content

What are generators? Creating a generator. Use of generators. Closing a generator.


What are generators?

top

ES6 introduced a new way of working with functions and iterators in the form of generators (or generator functions).

A generator is a function that can stop midway and then continue from where it stopped.
A generator appears to be a function but it behaves like an iterator.

In JavaScript, a generator is a function which returns an object, on which you can call next(). Every invocation of next() will return an object of shape.

Generator functions behave differently from regular ones. When such function is called, it doesn't run its code. Instead it returns a special object, called "generator object", to manage the execution.


Creating a generator.

top

For creating a generator function, function * syntax is used instead of just function:

        function* generateSequence() {
            yield 1;
            yield 2;
            return 3;
        }
        

Any number of spaces can exist between the function keyword, the *, and the function name.
Since it is just a function, it can be used anywhere that a function can be used, i.e inside objects, and class methods.

Inside the function body, there is no "return". Instead, another keyword "yield" is used. It's an operator with which a generator can pause itself. Every time a generator encounters a "yield", it "returns" the value specified after it. However, in the context of generators it is said that "the generator has yielded ... ".

There can also be a return from a generator. However, "return" sets the "done" property to true, after which the generator cannot generate any more values.

The main method of a generator is next(). When called, it runs the execution until the nearest yield <value> statement (value can be omitted, then it's undefined). Then the function execution pauses, and the yielded value is returned to the outer code. The result of next() is always an object with two properties: value: the yielded value; done: true if the function code has finished, otherwise false.

example:

see console

code:
                <script>
                    function * generatorFunction() { // Line 1
                            console.log('This will be executed first.'); 
                            yield 'Hello, ';   // Line 2
                            console.log('I will be printed after the pause');  
                            yield 'World!';
                    }
                    const generatorObject = generatorFunction(); // Line 3
                    console.log(generatorObject.next().value); // Line 4  
                    console.log(generatorObject.next().value); // Line 5 
                    console.log(generatorObject.next().value); // Line 6
                           
                    // This will be executed first.
                    // Hello, 
                    // I will be printed after the pause
                    // World!
                    // undefined
        
                    function* generate() {
                        yield 1;
                        yield 2;
                        return 3;
                    }
                    // "generator function" creates "generator object"
                    let generator_1 = generate();
                    
                    console.log(generator_1); // [object Generator]
                </script>
            


Use of generators.

top

Implementing iterables

Since generators are iterables, they can help to simplify the code for implementing iterator.

Generators are iterable so you can use them with the for...of loop.

example: implement iterables

code:
                <div>
                    <p class="spec" id="generator01"></p>
                    <p class="spec" id="generator01A"></p>
                </div>
                <script>
                    function* iterableObj() {
                        yield 'This';
                        yield 'is';
                        yield 'iterable.'
                    }
                    for (const val of iterableObj()) {
                        document.getElementById("generator01").innerHTML += val + " ";
                        console.log(val);
                    }
        
                    let range = {
                        from: 1,
                        to: 5,
        
                        *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
                            for(let value = this.from; value <= this.to; value++) {
                                yield value;
                            }
                        }
                    };
                    document.getElementById("generator01A").innerHTML = [...range];
                           
                </script>
            

Better async functionality

Generators have a more extensive array of capabilities than asynchronous functions, but are capable of replicating similar behavior. Implementing asynchronous programming in this way can increase the flexibility of the code.

Using generators, something can be created that does not use the "async/await" keywords. Instead, it will use a newly created function and "yield" values instead of await promises.

example: async functionality

code:
                <div>
                    <p class="spec" id="generator04"></p>
                    <p class="spec" id="generator04A"></p>
                    <p class="spec" id="generator04B"></p>
                    <p class="spec" id="generator04C"></p>
                    <p class="spec" id="generator04D"></p>
                </div>
                <script>
                    function* generatorFunction() {
                        yield 'Neo'
                        yield 'Morpheus'
                        yield 'Trinity'

                        return 'The Oracle'
                    }

                    const generator = generatorFunction()
                    document.getElementById("generator04").innerHTML = generator.next().value;
                    document.getElementById("generator04A").innerHTML = generator.next().value;
                    document.getElementById("generator04B").innerHTML = generator.next().value;
                    document.getElementById("generator04C").innerHTML = generator.next().value;
                    document.getElementById("generator04D").innerHTML = generator.next().value;
                </script>
            

Infinite data streams

One of the useful aspects of generators is the ability to work with infinite data streams and collections. This can be demonstrated by creating an infinite loop inside a generator function that increments a number by one.

With generators, there are no worries about creating an infinite loop, because execution can be halted and resumed at will.

However, be cautious with how to invoke the generator. If "spread" or "for...of" is used on an infinite data stream, iteration over an infinite loop will happen all at once, which will cause the environment to crash.

example: infinite data streams

code:
                <div>
                    <p class="spec" id="generator05"></p>
                    <p class="spec" id="generator05A"></p>
                    <p class="spec" id="generator05B"></p>
                    <p class="spec" id="generator05C"></p>
                    <p class="spec" id="generator05D"></p>
                    <p class="spec" id="generator05E"></p>
                </div>
                <script>
                    function* naturalNumbers() {
                        let num = 1;
                        while (true) {
                            yield num;
                            num = num + 1
                        }
                    }
                    const numbers = naturalNumbers();
                    console.log(numbers.next().value)
                    console.log(numbers.next().value)
                        // 1
                        // 2
                    document.getElementById("generator05").innerHTML = numbers.next().value;
                    document.getElementById("generator05A").innerHTML = numbers.next().value; 
                        // 3
                        // 4

                    function* pseudoRandom(seed) {
                        let value = seed;
                        while(true) {
                            value = value * 16807 % 2147483647
                            yield value;
                        }

                    };
                    let generator_2 = pseudoRandom(1);
                    document.getElementById("generator05B").innerHTML = generator_2.next().value; 
                    document.getElementById("generator05C").innerHTML = generator_2.next().value;
                    document.getElementById("generator05D").innerHTML = generator_2.next().value;
                    document.getElementById("generator05E").innerHTML = generator_2.next().value;
                </script>
            


Closing a generator.

top

A generator can have its "done property" set to true and its "status" set to closed by iterating through all its values. There are two additional ways to immediately cancel a generator: with the return() method, and with the throw() method.

With return(), the generator can be terminated at any point, just as if a return statement had been in the function body. The return() method forces the generator object to complete and to ignore any other "yield" keywords.

If the body of a generator function has a way to catch and deal with errors, you can use the throw() method to throw an error into the generator.
This starts up the generator, throws the error in, and terminates the generator.

example: closing a generator

code:
                <div>
                    <p class="spec" id="generator06"></p>
                    <p class="spec" id="generator06A"></p>
                    <p class="spec" id="generator06B"></p>
                    <p class="spec" id="generator06C"></p>
                    <p class="spec" id="generator06D"></p>
                    <p class="spec" id="generator06E"></p>
                    <p class="spec" id="generator06F"></p>
                </div>
                <script>
                    function* genFunction() {
                        yield 'Neo'
                        yield 'Morpheus'
                        yield 'Trinity'
                    }

                    const generator_A = genFunction()
                    document.getElementById("generator06").innerHTML = generator_A.next().value;
                    document.getElementById("generator06A").innerHTML = generator_A.return("There is nothing"); 
                    document.getElementById("generator06B").innerHTML = generator_A.next().value;

                    function* generateFunction() {
                        try {
                            yield 'Neo'
                            yield 'Morpheus'
                        } catch (error) {
                            console.log(error);
                        }
                        }

                        // Invoke the generator and throw an error
                        const generator_B = generateFunction();
                        document.getElementById("generator06D").innerHTML = generator_B.next().value;
                        document.getElementById("generator06E").innerHTML = generator_B.throw(new Error('Agent Smith!'));
                        document.getElementById("generator06F").innerHTML = generator_B.next().value;

                </script>