cover

Introduction

During my daily work, like every developer, we discover a lot of precious online material. Sometimes it contains suggestions, fragments of code or even complete tutorials. However, rarely read articles that open your mind and/or change the development prospect.

Luckly I found out an incredible presentation from Anjana Vakil (and the related video) that pushed me to revaluate a relatively old javascript feature that I had, until now, understimated that are generators.

Below the summary of the main topics about generators explained through code samples as inspired by original article

Considerations

  • generators are old but still innovative 🚀
  • generators are underappreciated 🤨 
  • generators are actually useful 👍🏻    
  • generators are great teachers 🎓
  • generators are mind-blowing 🤯

Basics

generator object is a type of iterator

It has a .next() method returning { value:any, done:boolean } object

someIterator.next()
// { value: 'something', done: false }
someIterator.next()
// { value: 'anotherThing', done: false }
someIterator.next()
// { value: undefined, done: true }

generator functions return generator objects

function* defines a generator function that implictly return a generator object

function* genFunction() {
    yield "hello world!";
}
let genObject = genFunction();
// Generator { }
genObject.next();
// { value: "hello world!", done: false }
genObject.next();
// { value: undefined, done: true }

generator actions

.next() advances ▶️; yield pauses ⏸️; return stops ⏹️

function* loggerator() {
    console.log('running...');
    yield 'paused';
    console.log('running again...');
    return 'stopped';
}
let logger = loggerator();
logger.next(); // running...
// { value: 'paused', done: false }
logger.next(); // running again...
// { value: 'stopped', done: true }

generators are also iterable

generator object returned by generator function behaves like an iterator hence is iterable

function* abcs() {
    yield 'a';
    yield 'b';
    yield 'c';
}
for (let letter of abcs()) {
    console.log(letter.toUpperCase());
}
// A
// B
// C
[...abcs()] // [ "a", "b", "c" ]

Generators for consume data

Below we will evaluate how to use generators to consume data

custom iterables with @@iterator

Evaluate how to implement custom iterable objects powerd by generators

Example: Create a CardDeck object

cardDeck = ({
    suits: ["♣️", "♦️", "♥️", "♠️"],
    court: ["J", "Q", "K", "A"],
    [Symbol.iterator]: function* () {
        for (let suit of this.suits) {
            for (let i = 2; i <= 10; i++) yield suit + i;
            for (let c of this.court) yield suit + c;
        }
    }
})
> [...cardDeck]
Array(52) [
"♣️2", "♣️3", "♣️4", "♣️5", "♣️6", "♣️7", "♣️8", "♣️9", "♣️10", "♣️J", "♣️Q", "♣️K", "♣️A", 
"♦️2", "♦️3", "♦️4", "♦️5","♦️6", "♦️7", "♦️8", "♦️9", "♦️10", "♦️J", "♦️Q", "♦️K", "♦️A",
"♥️2", "♥️3", "♥️4", "♥️5","♥️6", "♥️7", "♥️8", "♥️9", "♥️10", "♥️J", "♥️Q", "♥️K", "♥️A",
"♠️2", "♠️3", "♠️4", "♠️5","♠️6", "♠️7", "♠️8", "♠️9", "♠️10", "♠️J", "♠️Q", "♠️K", "♠️A" 
]

lazy evaluation & infinite sequences

Since the generator are lazy evaluated (they weak up only when data is required) we can implement somtehing of awesome like an infinite sequence.

Below some examples that make in evidence how is simple an powerful combine generators and iterators

infinityAndBeyond = ƒ*()

function* infinityAndBeyond() {
    let i = 1;
    while (true) {
        yield i++;
    }
}

take = ƒ*(n, iterable)

function* take(n, iterable) {
    for (let item of iterable) {
        if (n <= 0) return;
        n--;
        yield item;
    }
}

take first N integers

let taken = [...take(5, infinityAndBeyond())]
taken = Array(5) [1, 2, 3, 4, 5]

map = ƒ*(iterable, mapFn)

function* map(iterable, mapFn) {
    for (let item of iterable) {
        yield mapFn(item);
    }
}

square first N integers

let squares = [
    ...take( 9, map(infinityAndBeyond(), (x) => x * x) )
]

squares = Array(9) [1, 4, 9, 16, 25, 36, 49, 64, 81]

recursive iteration with yield*

It is very interesting that we can yield data in recursive way as shown in example below generating a tree object

binaryTreeNode = ƒ(value)

function binaryTreeNode(value) {
    let node = { value };
    node[Symbol.iterator] = function* depthFirst() {
        yield node.value;
        if (node.leftChild) yield* node.leftChild;
        if (node.rightChild) yield* node.rightChild;
    }
    return node;
}

tree = Object { value, leftChild, rightChild }

tree = {
    const root = binaryTreeNode("root");
    root.leftChild = binaryTreeNode("branch left");
    root.rightChild = binaryTreeNode("branch right");
    root.leftChild.leftChild = binaryTreeNode("leaf L1");
    root.leftChild.rightChild = binaryTreeNode("leaf L2");
    root.rightChild.leftChild = binaryTreeNode("leaf R1");
    return root;
}
> [...tree]
Array(6) [
    "root", 
    "branch left", 
    "leaf L1", 
    "leaf L2", 
    "branch right", 
    "leaf R1"
    ]

async iteration with async iterator

And, of course, could not be missed compliance of generators with asynchronous iterations 💪

In the example below we will fetch asynchronusly starwars ships names from web using async iterator powered by generator

getSwapiPagerator = ƒ(endpoint)

getSwapiPagerator = (endpoint) =>
    async function* () {
        let nextUrl = `https://swapi.dev/api/${endpoint}`;
        while (nextUrl) {
            const response = await fetch(nextUrl);
            const data = await response.json();
            nextUrl = data.next;
            yield* data.results;
        }
    }

starWars = Object {characters: Object, planets: Object, ships: Object}

starWars = ({
    characters: { [Symbol.asyncIterator]: getSwapiPagerator("people") },
    planets: { [Symbol.asyncIterator]: getSwapiPagerator("planets") },
    ships: { [Symbol.asyncIterator]: getSwapiPagerator("starships") }
})

fetch star wars ships

{
    const results = [];
    for await (const page of starWars.ships) {
        console.log(page.name);
        results.push(page.name);
        yield results;
    }
}
Array(36) [
  0: "CR90 corvette"
  1: "Star Destroyer"
  2: "Sentinel-class landing craft"
  3: "Death Star"
  4: "Millennium Falcon"
  5: "Y-wing"
  6: "X-wing"
  7: "TIE Advanced x1"
  8: "Executor"
  9: "Rebel transport"
  10: "Slave 1"
  11: "Imperial shuttle"
  12: "EF76 Nebulon-B escort frigate"
  13: "Calamari Cruiser"
  14: "A-wing"
  15: "B-wing"
  16: "Republic Cruiser"
  17: "Droid control ship"
  18: "Naboo fighter"
  19: "Naboo Royal Starship"
  20: "Scimitar"
  21: "J-type diplomatic barge"
  22: "AA-9 Coruscant freighter"
  23: "Jedi starfighter"
  24: "H-type Nubian yacht"
  25: "Republic Assault ship"
  26: "Solar Sailer"
  27: "Trade Federation cruiser"
  28: "Theta-class T-2c shuttle"
  29: "Republic attack cruiser"
  30: "Naboo star skiff"
  31: "Jedi Interceptor"
  32: "arc-170"
  33: "Banking clan frigte"
  34: "Belbullab-22 starfighter"
  35: "V-wing"
]

Generators for produce data

so we have understood that generators are a great way to produce data but they can also consume data 😏

keep in mind that yield is a two-way street

It is enough pass in a value with .next(input) 😎. See example below

function* listener() {
    console.log("listening...");
    while (true) {
        let msg = yield;
        console.log('heard:', msg);
    }
}
let l = listener();
l.next('are you there?'); // listening...
l.next('how about now?'); // heard: how about now?
l.next('blah blah'); // heard: blah blah

generators remember state - state machines

Like classical javascript function within generator function’s scope we can store a state.

function* bankAccount() {
    let balance = 0;
    while (balance >= 0) {
        balance += yield balance;
    }
    return 'bankrupt!';
}
let acct = bankAccount();
acct.next(); // { value: 0, done: false }
acct.next(50); // { value: 50, done: false }
acct.next(-10); // { value: 40, done: false }
acct.next(-60); // { value: "bankrupt!", done: true }

Generators cooperative features

Summarizing we can say that generator funcions are perfect enabler for cooperative work and in particular :

  • generators can yield control and get it back later ✅
  • generators can function as coroutines
  • generators allow to pass control back and forth to cooperate

Example: Actor-ish message passing!

This last example is simple implementation of an actor based system based on a shared queue

let players = {};
let queue = [];

function send(name, msg) {
    console.log(msg);
    queue.push([name, msg]);
}

function run() {
    while (queue.length) {
        let [name, msg] = queue.shift();
        players[name].next(msg);
    }
}

function* knocker() {
    send('asker', 'knock knock');
    let question = yield;
    if (question !== "who's there?") return;
    send('asker', 'gene');
    question = yield;
    if (question !== "gene who?") return;
    send('asker', 'generator!');
}

function* asker() {
    let knock = yield;
    if (knock !== 'knock knock') return;
    send('knocker', "who's there?");
    let answer = yield;
    send('knocker', `${answer} who?`);
}

players.knocker = knocker();
players.asker = asker();
send('asker', 'asker get ready...'); // call first .next()
send('knocker', 'knocker go!'); // start the conversation
run();
// asker get ready...
// knocker go!
// knock knock
// who's there?
// gene
// gene who?
// generator!

Conclusions

generators have practical uses

  • custom iterables
  • lazy/infinite sequences
  • state machines
  • data processing
  • data streams

generators can help you

  • control flow & async
  • coroutines & multitasking
  • actor models
  • systems programming
  • functional programming

I think generator function is a powerful tool in javascript eco-system that must be taken into consideration. I hope this can be useful like has been to me, in the meantime ** happy coding ** 👋

Resources