DEV Community

Cover image for The power of "js generators" revamped
bsorrentino
bsorrentino

Posted on

The power of "js generators" revamped

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 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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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" ]
Enter fullscreen mode Exit fullscreen mode

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;
        }
    }
})
Enter fullscreen mode Exit fullscreen mode
> [...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" 
]
Enter fullscreen mode Exit fullscreen mode

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++;
    }
}
Enter fullscreen mode Exit fullscreen mode

take = ƒ*(n, iterable)

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

take first N integers

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

map = ƒ*(iterable, mapFn)

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

square first N integers

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
> [...tree]
Array(6) [
    "root", 
    "branch left", 
    "leaf L1", 
    "leaf L2", 
    "branch right", 
    "leaf R1"
    ]
Enter fullscreen mode Exit fullscreen mode

async iteration with @@asyncIterator

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;
        }
    }
Enter fullscreen mode Exit fullscreen mode

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

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

fetch star wars ships

{
    const results = [];
    for await (const page of starWars.ships) {
        console.log(page.name);
        results.push(page.name);
        yield results;
    }
}
Enter fullscreen mode Exit fullscreen mode
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"
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)