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 pairsconst values = map.values(); // Iteratorconst 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 timesforEach(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:
- Numbers.count: counts up integers, eg
count(3)
yields0, 1, 2
. - Oscillators
Forming generators
ixfx has some functions for specifically making a generator from various sources:
- Iterables.fromArray - yields values from an array at a given rate.
- Iterables.fromEvent - yields values from an event.
- Iterables.fromIterable - yields values from a source iterable at a given rate.
- Iterables.fromFunction - yields values using a function.
- Iterables.fromFunctionAwaited - yields values from an asynchronous function.
…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 valuer.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}