full-stack overflow

24 Feb 2018

ES6 — Generator Functions

Generator Functions

A generator function is a function that can exit and re-enter execution at a later time. When the function executes later on, it retains its memory of its local variables. In this sense, it acts like a closure.

The best way to understand generators is with some simple examples.

Defining Generation

The syntax for defining a generator function is to use a * before the function name and then have yield statements in the function body.

function* generate() {
  yield "this";
  yield "is";
  yield "a";
  yield "sentence";
}

We instantiate the generator like so:

let G = generate();
G.next();

Calling G.next() returns an object with a value and a done property. value corresponds to the current value yielded by the generator function. done is a boolean signifying whether the generator function is exhausted for not.

Calling G.next() sequentially in our example function yields:

{value: "this", done: false}
{value: "is", done: false}
{value: "a", done: false}
{value: "sentence", done: false}
{value: undefined, done: true}

Note that done is not true until we call the function and there are no more yields left.

Generators With Arguments

Generators can also take arguments.

function* AddOneMore(a) {
  yield a + 1;
  yield a + 2;
  yield a + 3;
}

let v = AddOneMore(1);
v.next(); // {value: 2, done: false}
v.next(); // {value: 3, done: false}
v.next(); // {value: 4, done: false}

Generators With Returns

If a return statement is included in a generator, the generator will be exhausted when the return statement is reached. Unlike a generator that only contains yields, when the return statement is reached, the object will have done true and the value returned.

function* returnedG(arg) {
  yield 1;
  return arg;
}

let v = returnedG(2);
v.next(); // {value: 1, done: false}
v.next(); // {value: 2, done: true}
v.next(); // {value: undefined, done: true}

Infinite Generators

An infinite loop in a generator function can be used to return the results of an infinite series. For example, we can create an infinite list of IDs and return the next ID each time the generator function is called.

function* idGenerator() {
  let id = 0;
  while (true) {
    yield ++id;
  }
}

Generate infinite sequence of numbers from 0 - 10

function* zeroToTen() {
  let id = 0;
  while (true) {
    yield id >= 10 ? (id = 0) : ++id;
  }
}

Generate recurring numbers 0-10

function* fibbo() {
  let a = 0;
  let b = 1;
  while (true) {
    let current = a;
    a = b;
    b = current + a;
    yield current;
  }
}

Generator for the Fibonacci sequence

Generators Yielding To Other Generators

Generators can even yield execution to other generators.

function* a() {
  yield 1;
  yield* b();
  yield "a is now in control";
}

function* b() {
  yield "b is now in control";
}

let backAndForth = a();
backAndForth.next(); // 1
backAndForth.next(); // "b is now in control"
backAndForth.next(); // "a is now in control"
backAndForth.next(); // {value: undefined, done: true}

But, why?

Generators are more than just syntactic sugar. They are powerful tools for making infinite sequences or tasks manageable by implementing lazy evaluation, the paradigm of only producing results when they are requested. Lazy Evaluation is why you can have a while (true) loop in the function that doesn’t hang infinitely: yield breaks out of the while loop until the next time the function is called.

Look for ways that you can implement generators to make your code more readable. Consider how you might use it with asynchronous code like fetch or use it to make more readable functions that perform similar tasks.