Monday 5 August 2024

JavaScript Return From Generator

Recently I came across a discussion about using the return value from a a generator. This applies both to Python and JavaScript, but in this post I'll focus on JavaScript. We know that generators yield values, but they can also return a value. So far I had only used return statements in generators to finish/close on condition, just a "return;" without a value. But you can do a "return x;" and that x will show up in the object returned in the last iteration (the one that indicates that the iteration has finished). That means that rather than the typical {done: true, value: undefined} you'll get a {done: true, value: x}. The thing is that the most common way to iterate a generator, the for-of loop, won't give us access to that value. Let's see an example:


function* citiesGen() {
    yield "Paris";
    yield "Porto";
    return "Europe";
}

console.log("- for of loop");

cities = citiesGen();
for (let city of cities) {
    console.log(city);
} 
console.log(cities.next())
// Paris
// Porto
// { value: undefined, done: true }

We iterate the yielded values, and the loop stops when the generator returns a {done: true, value: "Europe"}, but the loop does not give us acces to that value. If after the loop we invoke next() again, it will return this object: {done: true, value: undefined}, the value is undefined, no longer "Europe", so we've lost it.

To circunvent this problem we can use a less elegant while loop rather than a for-of, like this:


let city;
cities = citiesGen();
while (!city?.done) {
    city = cities.next();
    if (!city.done) {
        console.log(city.value);
    }
}
console.log(`return value: ${city.value}`) 

That works fine, but that while loop looks a bit ugly. We can wrap that logic into a class implementing the Iteration Protocols and providing access to the returned value. That way we can use a for-of loop, like this:


class GeneratorWithReturn {
    constructor(generatorFn) {
        this.generatorFn = generatorFn;
        this.result = undefined;
    }

    *[Symbol.iterator]() {
        let item;
        let generatorOb = this.generatorFn();       
        while (!item?.done) {
            item = generatorOb.next();
            if (!item.done) {
                yield item.value;
            }
        }
        this.result = item.value;        
    }
}

cities = new GeneratorWithReturn(citiesGen);
for (let city of cities) {
    console.log(city);
} 
console.log(cities.result);

//Paris
//Porto
//return value: Europe 

That looks much better, but we can rewrite our iterator in a very concise way by leveraging the yield* operator, that delegates to another iterable object. The very interesting thing is that with yield* we get acces to the yielded values, and also to the returned value, like this:


class GeneratorWithReturn {
    constructor(generatorFn) {
        this.generatorFn = generatorFn;
        this.result = undefined;
    }

    *[Symbol.iterator]() {
        this.result = yield* this.generatorFn();        
    } 
}

cities = new GeneratorWithReturn(citiesGen);
for (let city of cities) {
    console.log(city);
} 
console.log(cities.result);

//Paris
//Porto
//return value: Europe

This feature of returning a value from a generator does equally apply to async iterators. And this is really useful, cause while I don't see particular real use cases for returning a value in normal iterators, in async iterators it allows us to express very nicely the idea of an async function that produces (yields) intermediate values (for example completion percentages) and a final return value. Imagine we have an asynchronous process that calculates several destinations to return a travel plan. Apart from that final "travel plan" we want to get each destination as it gets "calculated. We can express it with an async generator like this:


function aSleep(delay) {
    return new Promise(resolve => setTimeout(resolve, delay))
}

async function* designVacations(){
    let destinations = []
    await aSleep(1000);
    destinations.push("Paris");
    yield "destination found: Paris";
    await aSleep(1000);
    destinations.push("Porto");
    yield "destination found: Porto";
    return destinations.join(" -> ");
}


We'll use it with an adapted version of the class that we've just seen, but that this time providing the Asynchronous Iteration


class AsyncGeneratorWithReturn {
    constructor(generatorFn) {
        this.generatorFn = generatorFn;
        this.result = undefined;
    }

    // notice that though I'm not using await inside the function I have to mark it as async
    // otherwise I get an error: TypeError: yield* (intermediate value) is not iterable
    async *[Symbol.asyncIterator]() {
        this.result = yield* this.generatorFn();        
    } 
}


(async function asyncMain() {
    vacationsProcess = new AsyncGeneratorWithReturn(designVacations);
    for await (let city of vacationsProcess) {
        console.log(city);
    } 
    console.log(`return value (travelPlan): ${vacationsProcess.result}`);
}

// destination found: Paris
// destination found: Porto
// return value (travelPlan): Paris -> Porto


Notice that we use yield* to delegate to another async iterator, there's not a await yield* kind of operator. This is pretty nice as the same operator works for delegating to normal or async iterators. Another point to notice is that though we don't have any await in our asyn generator method, it has to be marked as async: async *[Symbol.asyncIterator]().

No comments:

Post a Comment