The yield
keyword has existed in Python since version 2.2 (2001), and JavaScript 1.7 (2006) but dates back as far as 1975. Over the years I have periodically remembered the existence of generators, read a handful of articles introducing them, and promptly put them to the back of my mind as trivia rather than a useful tool. It was only recently that I feel I have understood the value they provide.
Articles primarily cover how to use generators, but very little wisdon on when to use generators. The obvious benefit to lazy evaluation only seems to pay-off when dealing with large-scale datasets – the ceremony of creating a generator seems unwieldy when iterating a collection is among the first things we learn in a language.
Managing State
I tried playing around with generators for myself when carrying out some copy-work, first transliterating from C
to TypeScript
, then refactoring as something more idiomatic and functional. The original code found here implements Bresenham’s line algorithm and allows for a callback to be passed in and evaluated at each step.
The first (functional) version was not too dissimilar - the data stored in the TCOD_bresenham_data_t *data
parameter was encapsulated into a Line
class to keep state-management together, and grouping x-y pairs into their own structure:
interface ICoord {
x: number;
y: number;
}
interface IStepResult {
position: ICoord;
isEnd: boolean;
}
class Line {
origin: ICoord;
current: ICoord;
dest: ICoord;
constructor (origin: ICoord, dest: ICoord) {
...
}
Step(): IStepResult {
...
}
}
To consume this, we have to create a new line object and iterate until we reach the end as follows:
let line = new Line(startPos, endPos);
let nextStep = line.Step();
while (!nextStep.isEnd) {
DoTheThing(nextStep.position);
nextStep = line.Step();
}
In this case, we created the Line
object to encapsulate the state of traversal but we still have to manage the traversal itself. As author of the Line
class, I should not have to ask the consumer to “please be nice and don’t ask for anything more”. Presumably, they would just keep receiving the same final step repeatedly, but this should not even be an option.
Iterables
To convert the Step()
function to a Generator required minimal changes. Instead of returning an IStepResult
, either return true
if we’ve reached the end, or yield
the next ICoord
, and rename the function to *Step()
. In itself, this doesn’t particularly help a consumer to step through the line, but it allows us define an iterator for our class which will look very familiar:
interface ICoord {
x: number;
y: number;
}
class Line {
origin: ICoord;
current: ICoord;
dest: ICoord;
constructor (origin: ICoord, dest: ICoord) {
...
}
*[Symbol.iterator](): Generator<ICoord, boolean> {
let next = this.Step().next();
while (!next.done) {
yield next.value;
next = this.Step().next();
}
return true;
}
*Step(): Generator<ICoord, boolean> {
...
}
}
The joy of this is that now we have the whole traversal of our line encapsulated within this class and we have a much more intuitive interface to extract the values from our line like so:
let line = new Line(origin, destination);
for (const point of line) {
DoTheThing(point);
}