Andrew's Blfog

Javascript generator functions

It has been a while since my last blog post!

I learned recently about generator functions, having successfully avoided them for over a decade. The basic idea in Javascript is that they are a function that returns a special iterable object. Iterable in this case meaning you can loop over it.


function* generatorFunctionOne () {
    yield "a";
    yield "b";
    yield "c";
    yield "d";
}

const g = generatorFunctionOne();

for (const yieldedItem of g) {
    console.log(yieldedItem);
}

/* 
 * Logs, in order:
 * - a
 * - b
 * - c
 * - d
*/

That's a rather contrived example, but more-or-less shows the base concept - each next call (or each iteration) will continue up to the next yield, and whatever that yield is will be provided as the iterable element, and can be repeated ad nauseum via a loop. So, for example, if you wanted to generate n random numbers...


function* randomNumberGenerator (max=Infinity) {
    let current = 0;
    while (current++ < max) {
        yield Math.random();
    }
}

const g = randomNumberGenerator(5);

// You can loop over them, or if you're confident the result of the generator will be finite, you can do things like
//  convert it into an array using the spread operator, etc: [ ... g ] for example will turn it into an array with all 5 random numbers.
for (const num of g) {
    console.log(num);
}

On first blush it sounds like a semi-convoluted way of creating arrays. It's really better than that!

If you were so inclined, you could totally write a polling function using generators:


function sleep (delayMS) {
    return new Promise(resolve => setTimeout(resolve, delayMS));
}

async function* doPoll(theFunction, delay=3000) {
    let shouldWait = false;
    const polling = true;
    while (polling) {
        if (shouldWait) {
            console.log("Sleeping");
            await sleep(delay);
            console.log("Awake!");
        }

        shouldWait = true;

        try {
            yield await theFunction();
        } catch (err) {
            console.error(err);
        }
    }
}

async function theFunctionToPoll () {
    console.log("Making request");
    return fetch("/robots.txt");
}

async function main () {
    const webFetcher = doPoll(theFunctionToPoll, 5000);

    for await (const response of webFetcher) {
        console.log(response);

        if (Math.random() > 0.9) {
            console.log("Ending polling");
            webFetcher.return();
        }
    }
}

main();

There's a huge benefit in using a generator function for something that could have some complex inner state that needs to be maintained between iterations. For example, doPoll above keeps track of whether or not it should wait- and it maintains that state between iterations, meaning there's no messy back-and-forth maintenance required, nor publicly accessible variables that could get muddled up by some other piece of code.

Honorary mention: for await, very handy for looping over iterables that contain Promises.

Read More...