Working with Generators
November 13, 2017Javascript Generator Series
This article is part of a series of articles about ES6 generators. The plan is to explore iterators and generators, starting with what they are and show casing different situations where they can be useful.
- Introduction to ES6 Iterators and Generators
- Working with Generators
- Async/Await with Generators
Introduction
Generators have a lot of great applications, such as the randomInts
generator below. In this article we’ll however force the example a bit by also combining this generator with a zip
generator to produce a rollCharacter
function for D&D character creation.
In D&D there are six different attributes: Strength, Constitution, Dexterity, Intelligence, Wisdom and Charisma. Each attribute is rolled by the use of 3d6 (roll three six sided dice and add the results).
The attributes will be modelled as an array of strings:
const characterAttributes = [
'Strength',
'Constituion',
'Dexterity',
'Intelligence',
'Wisdom',
'Charisma'
]
The result of calling rollCharacter
should work like this:
rollCharacter()
// [[ 'Strength', 17 ],
// [ 'Constituion', 6 ],
// [ 'Dexterity', 15 ],
// [ 'Intelligence', 12 ],
// [ 'Wisdom', 8 ],
// [ 'Charisma', 11 ]]
Infinite random number generator
The dice can be simulated as an infinite generator where each call to next()
simulates a roll. Our generator function will be called randomInt(lower, upper, seed)
and take a lower value, an upper value and a seed.
const randomSeed = () => Math.floor(Math.random() * 1000)
function* randomInts(lower, upper, seed = randomSeed()) {
while (true) {
const x = Math.sin(seed++) * 10000
const random = x - Math.floor(x)
yield Math.round(lower + random * (upper - lower))
}
}
const d20 = randomInts(1, 20)
d20.next().value // 13
d20.next().value // 1 - oh no, a fumble!
One benefit of defining a randomInt
as a generator instead of a normal function is that we can produce a reproducable stream of values by giving the generator the same seed value, since it’s keeping all it’s state internal. This is great for testing.
Combining iterators
Since both arrays (our characterAttributes
) and generators are Iterable it would be nice if we could simply iterate them together and create pairs like ['Strength', 13]
. One pair for each attribute.
Many functional languages have a function called zip
that does just that. While Javascript doesn’t come with zip
by default, Lodash has it. Unfortunately the Lodash version does not support iterators so let’s define our own.
zip
will take any number of Iterables and generate groups until any of the iterations are exhausted.
;[...zip(['a', 'b', 'c'], [1, 2, 3, 4])] // => [['a', 1], ['b', 2], ['c', 3]]
The ...
in front of zip
is the spread operator (also part of ES6) which will “spread out” all values generated by zip
.
We’ll start by implementing two helper generators, produceWhile
and mappedGenerator
.
produceWhile
The generator produceWhile
will continue to generate values through the produce
function until the predicate
returns false
.
function* produceWhile(produce, predicate) {
let value = produce()
while (predicate(value)) {
yield value
value = produce()
}
}
mappedGenerator
mappedGenerator
wraps another generator by mapping the values it produces with the mapper
function:
function* mappedGenerator(generator, mapper) {
let result = generator.next()
while (!result.done) {
yield mapper(result.value)
result = generator.next()
}
}
zip
With these two helpers in place we are ready to implement our zip
generator function.
function* zip(...iterables) {
// Get Iterators for each Iterable
const iterators = iterables.map((iterable) => iterable[Symbol.iterator]())
// Produce groups of iterator results [IterResult1, IterResult2, ...] where each
// IterResult is an object like { value: x, done: true|false }. Stop when any
// IterResult is marked as done.
const producer = produceWhile(
() => iterators.map((iterator) => iterator.next()),
(value) => value.every((v) => !v.done)
)
// Unbox the value from the iterator results
yield* mappedGenerator(producer, (value) => value.map((v) => v.value))
}
Notice that zip
makes use of the yield*
construct. This delegates generation to a another iterator, in this case the mapped generator.
Putting it all together
With the zip
implementation we can define our function as follows:
const zip = require('./zip')
const randomInts = require('./randomInts')
function rollCharacter() {
const attributes = ['Strength', 'Constituion', 'Dexterity', 'Intelligence', 'Wisdom', 'Charisma']
return [...zip(attributes, randomInts(3, 18))]
}
rollCharacter()
// [[ 'Strength', 17 ],
// [ 'Constituion', 6 ],
// [ 'Dexterity', 15 ],
// [ 'Intelligence', 12 ],
// [ 'Wisdom', 8 ],
// [ 'Charisma', 11 ]]
Here we combine a finite Iteratable attributes
with an infinite one randomInts
. Since zip
stops when either is exhausted, it’s safe to use it together with the infinite one and the spread operator.