Published on

Generators

1391 words7 min read
Authors
  • avatar
    Name
    Curtis Warcup
    Twitter

Generators are functions that you can use for controlling the iterator. In comparison with regular functions that return a single value or nothing, generators return multiple values in a sequence, one after another. They operate great with iterables and allow creating data streams with ease.

Good resource is at Babel:here

Defining a Generator

For creating a generator, it is necessary to use a particular syntax construct. Generators have a * to the left of the function name function *numbers() OR right of the function keyword function*.

Generator functions are different from your typical function. Every time a generator is called, it doesn’t run its code. Alternatively, it returns a particular object, called “generator object” for managing the execution.

For example:

function* generate() {
  yield 1
  yield 2
  return 3
}
// "generator function" creates "generator object"
let generator = generate()
console.log(generator) // [object Generator]

When we define a generator, we call .next() on it which runs the code inside the function until it hits a yield statement. Afterward, the function execution suspends, and the yielded value on its turn is returned to the outer code.

The result of next() is always an object with the following two properties:

  • the value: that is the yielded value;
  • done: true in case the function code has finished; otherwise, it’s false.
function* numbers() {
  const result = 1 + 1
  return 20 + (yield result)
}
const generator = numbers()
console.log(generator.next()) // { value: 2, done: false }

When a yield statement is found, the generator is paused and the value of the yield is returned.

You can call .next() multiple times:

function* numbers() {
  const result = 1 + 1
  return 20 + (yield result)
}
const generator = numbers()
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: NaN, done: true }

// easier case
function* generate() {
  yield 1
  yield 2
  return 3
}
let generator = generate()
let oneValue = generator.next()
console.log(JSON.stringify(oneValue)) // {value: 1, done: false}
let twoValue = generator.next()
console.log(JSON.stringify(twoValue)) // {value: 2, done: false}
let threeValue = generator.next()
console.log(JSON.stringify(threeValue)) // {value: 3, done: true}

But if you call .next() with a value this time, you can think of the value replacing the entire yield statement.

function* numbers() {
  const result = 1 + 1
  return 20 + (yield result)
}
const generator = numbers()
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next(10)) // { value: 30, done: true }

This can be useful if you implement multiple yield statements in a function.

function* list() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
}

const generator = list()

console.log(generator.next()) // { value: 1, done: false }
console.log(generator.next()) // { value: 2, done: false }
console.log(generator.next()) // { value: 3, done: false }
console.log(generator.next()) // { value: 4, done: false }
console.log(generator.next()) // { value: 5, done: false }
console.log(generator.next()) // { value: undefined, done: true }

Why are they useful?

Can be used like so to iterate over a list:

function* list() {
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
}

const generator = list()

const numbers = []
for (let value of generator) {
  numbers.push(value)
}
numbers // [1, 2, 3, 4, 5]

Nested Generators

function* numbers() {
  yield 1
  yield 2
  yield* moreNumbers()
  yield 6
  yield 7
}

function* moreNumbers() {
  yield 3
  yield 4
  yield 5
}

const generator = numbers()

const values = []

for (let value of generator) {
  values.push(value)
}

values // [1, 2, 3, 4, 5, 6, 7]

yield* is a special syntax that allows you to call another generator function.

Real example - walking through a tree:

class Tree {
  constructor(value = null, children = []) {
    this.value = value
    this.children = children
  }

  *printValues() {
    yield this.value
    for (let child of this.children) {
      yield* child.printValues()
    }
  }
}

const tree = new Tree(1, [new Tree(2, [new Tree(4)]), new Tree(3)])

// I want to iterate over the tree and print the values.
// expect to see 1, 2, 4, 3

const values = []
for (let value of tree.printValues()) {
  values.push(value)
}

values // [1, 2, 4, 3]