Skip to content

Generators

Generators are a pattern in Javascript for producing values. They are interesting in that they allow a function to return a value whilst still running.

In terms of using generators, there’s not much difference between them and iterators. Here we will use them interchangeably.

Basics

Generators are often encountered as a way of iterating over data.

For example, we can iterate over the values or keys of a Map:

const map = new Map();
// ...fill map with key-value pairs
const values = map.values(); // Iterator
const keys = map.keys(); // Also an iterator

Once we have the generator/iterator, it’s common to use a for of loop to run through them:

for (const v of values) {
// do something with value 'v'
}

Or if it is asynchronous:

for await (const v of values) {
// do something with value 'v'
}

The downside of these loops is you’re sort of stuck in a very limited context. Sometimes you might need to manually step through an iterator. Deciding when and where to do so, instead of being locked into a loop.

For this, call next() on it:

const { value, done } = keys.next();
if (done) {
// Generator/iterator has completed, no data available
} else {
// 'value' ought to be something to use...
}

Using this pattern, we can grab a new item from the generator when needed. For example, imagine stepping through a generator each time the user taps a button. This would be harder to structure as a for of loop.

Another option is ixfx’s Iterables.forEach

import { forEach } from "https://unpkg.com/ixfx/dist/iterables.js"
// Do something with each key 'k'
forEach(keys, k => console.log(k));

This pattern is succinct when combining generators:

import { forEach } from "https://unpkg.com/ixfx/dist/iterables.js"
import { count } from "https://unpkg.com/ixfx/dist/numbers.js"
// Prints 'Hi' five times
forEach(count(5), i => console.log(`Hi`));

Get all the data

If you want to read all the data from a generator into an array:

Async generator

const data = await Array.fromAsync(generator);

Synchronous generator

const data = [...generator];
// Or:
const data = Array.from(generator);

Only do this if you know the generator will complete. Generators can be infinite, always producing a value. Instead use Iterables.toArray which can have limits for how many values to read or for how long.

Temporal

Flow.delayLoop is an asynchronous generator that delays its looping. It doesn’t return a usable value.

import { delayLoop } from "https://unpkg.com/ixfx/dist/flow.js"
for await (const o of delayLoop(1000)) {
// Do something every second
// Warning: loops forever
}
// Execution won't continue here until the loop is exited

Accessing values over time

Flow.repeat repeatedly calls a function or generator, yielding the result. It can have optional delays as well as a limit of how many items to fetch

For example, to access the value of source every second:

import { repeat } from "https://unpkg.com/ixfx/dist/flow.js"
const delayedGenerator = repeat(source, { delay: 1000 } );
for await (const v of delayedGenerator) {
// Gets the next value from `source` with 1 second of delay
}

Producers

Some of the generators that produce values in ixfx are:

Forming generators

ixfx has some functions for specifically making a generator from various sources:

…but many ixfx functions will return a generator as a result.

From scratch

You can also write your own generator very easily, by marking your function with a ’*’.

This generator will return endless random numbers

function* generateRandom() {
while (true) {
yield Math.random();
}
}

If we access the generator manually, we essentially pull a value from it:

const r = generateRandom();
r.next().value; // get a random value
r.next().value; // get another random value

Our generator function looks dangerous in that there is a loop that never exits. But what is interesting about generators is that code essentially suspends at the yield line until a value is requested.

In the above example, since we only request a value twice, our generator loop only runs twice.

Problems however occur if we use such an infinite generator in a for of loop which will run until the generator exits:

for (const v of generateRandom()) {
// this will loop forever
}
// ...and never get here.

One approach might be to limit the for of loop:

let count = 0;
for (const v of generateRandom()) {
count++;
if (count >= 5) break; // exit loop
}
// Code will continue here after 5 loops

Or to limit the generator:

function* generateRandom(count) {
while (count-- >= 0) {
yield Math.random();
}
}
for (const v of generateRandom(5)) {
// safe to use now, since generator
// ends on its own accord
}