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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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}