Friday 5 June 2020

Lazy Promises

Last year I mentioned in this post that one of the alleged advantages of using Observables over Promises for single values is that Observables are lazy (the "executor" function of an Observable won't be run until the Observable gets subscribed). Conversely, the "executor" function of a Promise gets executed as soon as the Promise constructor is invoked. As I said in that post, I don't think that we need the laziness much often, and for me the cleanliness of a code written with async-await absolutely beats Observables and RX (remember I'm taking about single values). But anyway what if we need laziness? Well, implementing a Lazy Promise is pretty simple.

We need to store the "executor" function when the Lazy Promise is created, and use it to create the real Promise when we first need it, that is, the first time "then" is invoked.

<
class LazyPromise extends Promise{

    constructor(creationFn){
        //compiler forces me to do a super call
        super(() => {});
        console.log("creating LazyPromise");
        this.initialized = false;
        this.creationFn = creationFn;
    }

    then(resFn, rejFn){
        if (!this.initialized){
            console.log("creating Real Promise");
            this.initialized = true;
            this.internalPromise = new Promise(this.creationFn);
        }

        return this.internalPromise.then(resFn, rejFn);

    }

}

function sleep(ms){
    let resolveFn;
    let pr = new Promise(res => resolveFn = res);
    setTimeout(() => resolveFn(), ms);
    return pr;
}


(async () => {
    let pr1 = new LazyPromise(res => {
        console.log("starting query");
        setTimeout(() => {
            console.log("finishing query");
            res("hi");
        }, 2000);
    });
    
    console.log("before sleep");
    await sleep(5000);
    console.log("after sleep");
    let result = await pr1;
    console.log("promise returns: " + result);
    
})();

As you can see, I'm inheriting from Promise. It's not that we are reusing anything in the Parent class, but well, a LazyPromise feels to me like a specialized Promise, so regardless of being in a type-checking free language, inheritance seems appropiate. Additionally I was thinking that in order for result = await myLazyPromiseInstance to work we would need it to inherit from Promise. I was assuming that the runtime would use instanceof to see that we were awaiting a Promise and if not wrap the value in a Promise (cause we know that we can await non-promise values). Well, indeed await does not check if it's a Promise, it checks if it's a thenable (it has a then method) in a nice example of duck typing, which makes pretty much sense. This thenable thing is mentioned in the MDN doc for Promise.resolve. This means that I can rewrite the above code using composition:

//we don't need to inherit from Promise, await works fine with just a "thenable"
//notice in the second part of the code that Promise.resolve of a thenable returns a new Promise rather than the thenable
class LazyPromise {

    constructor(creationFn){
        console.log("creating LazyPromise");
        this.initialized = false;
        this.creationFn = creationFn;
    }

    then(resFn, rejFn){
        if (!this.initialized){
            console.log("creating Real Promise");
            this.initialized = true;
            this.internalPromise = new Promise(this.creationFn);
        }

        return this.internalPromise.then(resFn, rejFn);

    }

}

function sleep(ms){
    let resolveFn;
    let pr = new Promise(res => resolveFn = res);
    setTimeout(() => resolveFn(), ms);
    return pr;
}

(async () => {
    let pr1 = new LazyPromise(res => {
        console.log("starting query");
        setTimeout(() => {
            console.log("finishing query");
            res("hi");
        }, 2000);
    });

    console.log("before sleep");
    await sleep(5000);
    console.log("after sleep");
    let result = await pr1;
    console.log("promise returns: " + result);
 })();

An additional point. As we know when we pass over a Promise to Promise.resolve, it returns that provided Promise rather than a new Promise. If we pass it over a thenable, it will return a new Promise, not just the thenable, but that new Promise "follows" the thenable (meaning that it gets resolved when the thenable is resolved.)

    let prA = new Promise(() => {});
    let prB = Promise.resolve(prA);
    console.log(prA === prB); //true

    //LazyPromise is a thenable
    prA = new LazyPromise(() => {});
    prB = Promise.resolve(prA);
    console.log(prA === prB); //false

    let prC = new LazyPromise(res => {
        console.log("starting query");
        setTimeout(() => {
            console.log("finishing query");
            res("Bye");
        }, 2000);
    });

    let prD = Promise.resolve(prC);
    result = await prD;
    console.log(result); //Bye
    //as explained in MDN, it wraps the thenable in a new Promise, but this new promise is not resolved until the thenable is resolved
    //the returned promise will "follow" that thenable, adopting its eventual state;
    

No comments:

Post a Comment