About generator functions in general

Generators are not often seen in every-day commercial code, and, to be honest, we can live well without them. In some cases, though, they can be useful and are, in fact, very powerful.

Recently, my friend Alex did a short demonstration on his great iterable library, which heavily relies on generator functions, so I decided to quickly write a short post on generators.

1. What is a generator function?

Generator is a function which can be exited and later re-entered. It keeps the state, and when it’s executed next time, it will pick up from the value where it was left off. They provide another layer of iteration and touch some deep, abstract principles of JavaScript.

Generators are marked with a little *, and the syntax looks like this:

function* myGeneratorFunc() {
  yield 'hello'
}

Generators return a generator object.

2. Iterables and iterators

A generator object has the features of both the iterable and iterator protocols.

2.1. Iterable protocol

An object is iterable if it has a method that is available through Symbol.iterator. This will make the object iterable, i.e. it can be iterated over using, for example, for ... of. This method will be called whenever the object needs to be iterated.

2.2. Iterator protocol

When this happens, an iterator will be returned, and the iterator is used to obtain the values through the next() method.

next() returns an object with two properties: value and done. If the generator has more values to generate (or yield, more on that below), value will contain the generated value, and done will be false. This basically means that there are more values left to return. When value is undefined, done will become true, and the generator has exhausted, and no more values left to return.

2.3. Examples

This sounds good, but it might not make much sense, so let’s take some examples.

This is a very basic iterable object:

const myIterable = {
  [Symbol.iterator]: function*() {
    yield 'hello'
    yield 'world'
    yield '!'
  }
}

myIterable has a property where the key is Symbol.iterator, and it’s a method (a generator function), so it fulfils the requirements.

Iterable objects can be iterated, and the above method (with the Symbol.iterator key) is getting called at iteration:

for (const word of myIterable) {
  console.log(word)
}

// hello
// world
// !

The spread operator works the same way under the hood, it calls the method with the Symbol.iterator key, too:

console.log(...myIterable) // 'hello' 'world' '!'

Some types, like strings are iterables by default:

const myString = 'hello'
for (const letter of myString) {
  console.log(letter)
}
// h
// e
// l
// l
// o

Similarly, we can spread them, even into an array:

console.log([ ...myString ]) // [ 'h', 'e', 'l', 'l', 'o' ]

Arrays, of course, are iterables:

const arr = [ 1, 2, 3 ]

for (let num of arr) {
  console.log(num)
}

// 1
// 2
// 3

and

console.log([ ...arr ]) // [ 1, 2, 3 ]

Sets, Map objects and typed arrays are also iterables.

But, numbers and objects are not:

const myNumber = 555

for (const digit of myNumber) {
  console.log(digit)
}

// TypeError: myNumber is not iterable

3. The use case I like the most

Generators can be used in asynchronous programming as well as to generate values.

3.1. Infinite number generator

The good thing about generators is that we can create an infinite number generator without getting into an infinite loop. As it was stated above, the generator function returns an iterator object, and the next() method can be called to get the value and the done properties. For the infinite number generator, value will always increment by 1 (or whatever increment is set up), and done will always be false:

const infiniteGenerator = function*() {
  let index = 1
  while (true) {
    yield index
    index++
  }
}

const gen = infiniteGenerator()
console.log(gen.next().value) // 1
console.log(gen.next().value) // 2
console.log(gen.next().value) // 3
console.log(gen.next().value) // 4
console.log(gen.next().value) // 5
console.log(gen.next().value) // 6
console.log(gen.next().value) // 7
console.log(gen.next().value) // 8
// and so on...

The spread operator cannot be used here, because there’s nothing to control how many numbers are extracted from the generator. Applying the spread operator will lead to an infinite loop.

3.2. Create ranges

Using the above principle, we can easily write a number range generator. The function below will generate the numbers between 1 and 5:

const range = function*(firstNumber, lastNumber) {
  let index = firstNumber
  while (index <= lastNumber) {
    yield index
    index++
  }
}

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

The numbers in the range can be accessed via gen.next().value. After all numbers have been yielded, value will become undefined indicating that the generator has exhausted.

It’s safe to use the spread operator here, because the while condition won’t always be true, and this way we can easily create an array of numbers:

console.log([...range(1, 5)]) // [ 1, 2, 3, 4, 5 ]

We don’t have to invoke range first and then call next, the spread operator does it all for us.

4. Summary

Although generators are not very often used, they can be useful in some cases, and the way they work goes down deep how JavaScript works.

Generators yield values which can be accessed via the next() method. Some types like strings, arrays or maps have generators built in and they are called iterables.

Thanks for reading, and see you next time.