CHATTBLOG

Generator functions for Iterables in Javascript

Technology
image
Javascript provides the Iterable protocol that allows easy and standard iteration mechanisms over custom objects. However, creating Iterables can have some repetitive overhead along with the effort required to maintain those parts. Read more about the Iterable protocol and how to create such objects in the article Javascript Iterables.
Generator functions allows developers to create Iterables without the overhead of implementing the protocol.
Implementing an Iterable for the first five whole numbers with generators looks like this.
  function* getFirstFiveWholeNumbers() {
         for(let i = 0; i < 5; i++) {
                 yield i;
         }
  }
 
  const firstFiveWholeNumbersIterable = getFirstFiveWholeNumbers();
  for(const wholeNumber of firstFiveWholeNumbersIterable) {
         console.log(wholeNumber);
  }
  // Log: 0
  // Log: 1
  // Log: 2
  // Log: 3
  // Log: 4
On the other hand, implementing it with the Iterable protocol is much more lines of code.
  const firstFiveWholeNumbersIterable = {
         index: 0,
         next: function() {
                 const done = this.index >= 5;
                 return { value: done ? undefined : this.index++, done };
         },
         [Symbol.iterator]: function() {
                 this.index = 0;
                 return this;
         }
  }
 
  for(const wholeNumber of firstFiveWholeNumbersIterable) {
         console.log(wholeNumber);
  }
  // Log: 0
  // Log: 1
  // Log: 2
  // Log: 3
  // Log: 4

Description

A generator function defined by the keyword function* is aGeneratorFunction type object and it returns a Generator object. This object implements both the iterable and iterator protocols, ie the returned object can be used both as an Iterable and anIterator.
The definition of the generator function allows the use of the keyword yield, which can be used multiple times and with each time it is used, it marks the value that should be provided by the Iterable.
The function is capable of pausing execution after the yieldkeyword is used, so that the next lines are executed only after the next() function of the iterator is called.
  function* myGenerator() {
     console.log(`Executed after function call`);
     yield 1;
     console.log(`Executed after yielding value 1`);
     yield 2;
     console.log(`Executed after yielding value 2`);
     return 3
  }
 
  const myIterator = myGenerator();
  // No log
 
  console.log(`Next value -> ${myIterator.next().value}`);
  // Log: Executed after function call
  // Log: Next value -> 1
 
  console.log(`Next value -> ${myIterator.next().value}`);
  // Log: Executed after yielding value 1
  // Log: Next value -> 2
 
  console.log(`Next value -> ${myIterator.next().value}`);
  // Log: Executed after yielding value 2
  // Log: Next value -> 3
 
  console.log(`Next value -> ${myIterator.next().value}`);
  // Log: Next value -> undefined
Note that the return statement causes the Iterator to yield the last value, after which if the next() function is called again, it recieves undefined as the value.

Convering an object into an iterable with generators

With the examples above we created iterables by calling generators, but what if an object already exists or requires to implement properties other than also being an iterable? By implementing the [Symbol.iterator] function property in an object, this can be done, but we have had to return an object containing the next() function from the [Symbol.iterator]function and this normal function does not have support for theyield keyword.
  const firstFiveWholeNumbersIterable = {
     wholeNumbers: ['zero', 'one', 'two', 'three', 'four'],
     [Symbol.iterator]: function() {
             let index = 0;
             return {
                     next: function() {
                             const done = index >= 5;
                             return { value: done ? undefined : index++, done };
                     }
             };
     }
  }
To be able to use yield keyword, convert the [Symbol.iterator]function into an generator function. As mentioned above, the generator function returns an object that is both iterable /iterator. The iterator implementation will be of use here.
  const firstFiveWholeNumbersIterable = {
     wholeNumbers: ['zero', 'one', 'two', 'three', 'four'],
     [Symbol.iterator]: function*() {
             for(let i = 0; i < 5; i++) {
                     yield i;
             }
     }
  }
 
  for(const wholeNumber of firstFiveWholeNumbersIterable) {
         console.log(wholeNumber);
  }
  // Log: 0
  // Log: 1
  // Log: 2
  // Log: 3
  // Log: 4

Delegating to iterables from within generator functions

Usually while calling generator functions recursively, it is required to call a generator function inside a generator function. The expected outcome is that the generator function will delegate to the internal iterable. The keyword yield* is used to indicate such a delegation.
Without using iterable delegation, the implementation of a generator function used to flatten an array will look like this.
  function* flattened(arr) {
     for(const val of arr) {
             if(Array.isArray(val)) {
                     for(const internalVal of flattened(val)) {
                             yield internalVal;
                     }
             } else {
                     yield val;
             }
     }
  }
By delegating the internal iterable, the innermost iteration can be removed.
  function* flattened(arr) {
     for(const val of arr) {
             if(Array.isArray(val)) {
                     yield* flattened(val);
             } else {
                     yield val;
             }
     }
  }

Passing values back to the generator

A values can be passed on to a generator by passing it as an argument to the next() function of the iterator created by the generator.
  function* multiplyUpToHundred() {
     let val = 1;
     let multiplyFactor = 1;
     while((val * multiplyFactor) <= 100) {
             val = val * multiplyFactor;
             multiplyFactor = (yield val) ?? 1;
     }
  }
 
  const multiplyUpToHundredIterator = multiplyUpToHundred();
  console.log(multiplyUpToHundredIterator.next());
  // Log: { value: 1, done: false }
  console.log(multiplyUpToHundredIterator.next(10));
  // Log: { value: 10, done: false }
  console.log(multiplyUpToHundredIterator.next());
  // Log: { value: 10, done: false }
  console.log(multiplyUpToHundredIterator.next(5));
  // Log: { value: 50, done: false }
  console.log(multiplyUpToHundredIterator.next());
  // Log: { value: 50, done: false }
  console.log(multiplyUpToHundredIterator.next(2));
  // Log: { value: 100, done: false }
  console.log(multiplyUpToHundredIterator.next(2));
  // Log: { value: undefined, done: true }

Async generators

Generator function can be async. The function internally handles such async implementations by returning a promise on next()function calls, for the iterator. This Promise further resolves to the result object containing the value and done properties.
  async function* asyncGenerator() {
     yield await Promise.resolve(1);
  }
  const asyncIterator = asyncGenerator();
  const { value } = await asyncIterator.next();
  console.log(value);
  // Log: 1
The for await...of statement allows iterations over asynciterables.
  async function* asyncGenerator() {
     yield await Promise.resolve(1);
     yield await Promise.resolve(2);
  }
 
  for await (const val of asyncGenerator()) {
         console.log(val);
  }
  // Log: 1
  // Log: 2

Summary

  • Generator functions are used to created iterables /iterators without having to implement the respective protocols.
  • The yield keyword is used to indicate the value that needs to be provided to the iterator.
  • The generator function execution pauses after a yieldstatement, and is triggered again when the next()function of the Iterator is called.
  • Generator functions can be provided to the[Symbol.iterator] property of objects, because the function return value implements the iterator protocol, along with the iterable protocol.
  • Generator functions delegate to iterables internally with the use of the yield* keyword.
  • Values passed into generators as argument to the next()function call of the iterator created by it and is received by the yield statement.
  • Generator functions can be async and the result of the call to the next() function of the iterator created by it is a Promise.
author image

Sujoy Chatterjee

Author