Categories
Codes General Javascript

Generator Functions in JavaScript

“Iteration, like friction, is likely to generate heat instead of progress.”

― George Eliot

Introduction

I’m very sure we are all familiar with a function which returns a single value, whether it is a string, number, boolean, object, etc. In JavaScript there is another type of function which can return (yield) multiple values on-demand and it allows us to create data streams with ease. These functions are generator functions.

What are Generator Functions?

I believe learning by reading examples is faster than theories. So, here is an example of a generator function:

function* generatorFunction(){
  yield '1st output!';
  yield '2st output!';
  return 'Last Output!';
}

First, note that there is an asterisk (*) symbol at the end of function. That is how to declare a generator function. Second, there are two yields in the function. Put it simply, yield is like return, it returns a value but it does not finish the function’s execution yet.

The diagram below shows how it works:

Normal Function vs Generator Function
Normal Function vs Generator Function

Or, see the snippet below for better explanation by code:

function* generatorFunction(){
  yield '1st output!';
  // ------------------- pauses on 1st call
  yield '2st output!';
  // ------------------- pauses on 2nd call
  return 'Last Output!';
  // ------------------- ends on 3rd call
}

A generator function can be called multiple times and gives different defined results, or even infinite results. It depends on the number of yields.

Next question, how do we get the outputs from a generator function? See the snippet below:

const gen = generatorFunction();
console.log(gen.next());
  /* { done: false, value: "1st output!" } */
console.log(gen.next());
  /* { done: false, value: "2st output!" } */
console.log(gen.next());
  /* { done: true, value: "Last Output!" } */
console.log(gen.next());
  /* { done: true, value: undefined } */

First, we create the generator object and assign it to the variable gen. Then, we get the result by calling gen.next(). The result is an object, gives the information whether the generator function is done or not, and of course the value of the yield output.

To get the next yield output, call .next() again. If the done property is true, it means that the generator function is finished. Another attempt of calling .next() will give value a result of undefined.

Usage

I’m sure the concept is quite easy to understand, but how can we implement generator functions in our code? What is it for?

Implement Iterables

When we want to implement iterators, we need to make an iterator object with .next(). And we also need to save the state manually. Imagine how complex it would be if your code is full of these. See this example below:

const iterableObject = {
  [Symbol.iterator]: () => {
    let counter = 0;
    return {
      next: () => {
        counter++;
        switch(counter){
          case 1: return { value: 'One', done: false };
          case 2: return { value: 'Two', done: false };
          case 3: return { value: 'Three', done: false };
        }
        return { value: 'No more.', done: true };
      }
    }
  },
}
for (const val of iterableObject) {
  console.log(val);
}

// One
// Two
// Three

Rather long and complicated, isn’t it? And here’s an implementation using generators, giving the same result, simpler, and more declarative:

function* iterableObject() {
  yield 'One';
  yield 'Two';
  yield 'Three'
}
for (const val of iterableObject()) {
  console.log(val);
}
// One
// Two 
// Three
Infinite Data Streams

We can create an iterator which never ends. See this example below:

function* oddNumbers() {
  let num = 1;
  while (true) {
    yield num;
    num = num + 2;
  }
}
const odd = oddNumbers();
console.log(odd.next().value); // 1
console.log(odd.next().value); // 3
console.log(odd.next().value); // 5

First, we create a generator function which has an infinite while loop. Inside the loop, we put a yield for the current value to be returned. Then, we calculate the next value to be returned: num = num + 2;. When we call next() again, we will get the next odd number. This continues as long as we want.

As Observers

Generators can become observers by giving values as arguments on the next() function. Here is a simple example:

function* observer() {
    while (true) {
        const input = yield;
        console.log(input);
    }
}
const obs = observer();
obs.next('a');
obs.next('b');
obs.next('c');
obs.next('d');

// b
// c
// d

First, we create a generator function with the same pattern as the infinite data streams section you read before. Second, see the statement: const input = yield;. This statement enables yield to get the value from the argument sent from next(). Third, it should be known that the argument of the first next() will always be ignored, because it is used to start the generator. Finally, we can call as many next() as we want.

This is just an example to understand the pattern. You can read more about the observer pattern with generator functions here.

Conclusion

Though it’s rarely used, generator functions are great for making iterable objects. A proper use could optimize our applications and simplify some codes we are working on.

Do you have any experience in implementing generator functions? What use case did you use it for? Let me know in the comments!

Live your code and code your life!

“Generate the belief in what you wish to achieve.”

― Steven Redhead, Life Is Simply A Game

By Ericko Yap

Just a guy who is obsessed to improve himself. Working as a programmer in a digital banking company. Currently programming himself in calisthenics, reading books, and maintaining a blog.

Leave a Reply

Your email address will not be published. Required fields are marked *