Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does the Promise.coroutine support generator as the yieldable value?

Promise.coroutine supports Promise as the yieldable value type. And via the addYieldHandler(function handler), Promise.coroutine can also support any types that retuning result only once. But how could I write a yieldHandler that can handle a generator type like co does?

like image 586
user552403 Avatar asked Sep 30 '14 02:09

user552403


1 Answers

First of all, Bluebird coroutines return promises that are of course yieldable on their own:

var foo = Promise.coroutine(function*(){
    yield Promise.delay(3000);
});

var bar = Promise.coroutine(function*(){
    yield foo(); // you can do this.
});

Generally, a generator is syntactic sugar for a function that returns an iterable sequence. There is nothing asynchronous about it in nature. You can write code that returns iterables in ES5 and even ES3 and consume it with yield in a generator.

Now - as for your question:

I would not recommend using Promise.coroutine like that.

Yielding arbitrary iterables from the specific iterable you're using is error prone, where wrapping it in Promise.coroutine makes it an explicit coroutine which you can easily yield. This is explicit, clear, and preserves all sorts of useful properties.

Yielding promises is not coincidental, and there is a good reason that async functions in ES7 await promises. Promises represent a temporal value and they're really the only thing waiting for makes sense - they represent exactly that - something you wait for. For that reason - explicitly waiting for promises is beneficial and makes for a solid abstraction.

If you do want to yield from an arbitrary generator - you don't need to add a yield handler via Bluebird.

Generators in JavaScript already have that ability built in using special notation, there is no need to add a specific exception to a specific promise library in order to yield from other iterables.

function genThreeToFour*(){
    yield 3;
    yield 4;
}
function genOneToFive*(){
    yield 1;
    yield 2;
    yield * genThreeToFour(); // note the *, delegate to another sequence
    yield 5;
}

Yielding from another sequence is already built into generators, if you want to yield from another generator you can simply yield * invoking it. That way it's explicit you're delegating. It's only one more character of code but it's a lot more explicit and it's also easier to step through with debuggers since the engine is aware of delegation. I still prefer yielding promises only but if you feel strongly about this - delegating to other generators sounds a lot better than yielding generators explicitly.

Note that this is also much faster than yielding generators explicitly since the conversion process of Promise.coroutine isn't cheap since it's meant to be executed once and then used so the conversion itself isn't fast but it produces a very fast function (faster than async in almost all use cases, and faster than how most people write callacks in most use cases. If you delegate instead of creating a coroutine every time and running it - you'll have better performance.

If you insist, you can addYieldHandler

It's quite possible to do this. If you're ok with the speed penalty and the (in my opinion worse) abstraction. You can use addYieldHandler to make it possible to yield generators.

First, the "good" way:

Promise.coroutine.addYieldHandler(function(gen) {
    if (gen && gen.next && gen.next.call ){ // has a next which is a function
        return Promise.try(function cont(a){
            var n = gen.next(a);
            if(n.done) return n.value; // `return` in generator
            if(!n.value.then) return cont(n.value); // yield plain value
            // handle promise case, propagate errors, and continue
            return n.value.catch(gen.throw.bind(gen)).then(cont); 
        });
    }
});

Here - we added the ability to yield iterables and not generators, the advantage is that functions that are not generators (for example - from third party libraries) but still produce iterables can still be yielded. The usage of the above is something along the lines of: yield generatorFn() where you invoke the generator. Note that our code here replicates what Promise.coroutine actually does and we almost ended up with a "native" implementation of it.

Now, if you want to yield generators, you can still do that:

var Gen = (function*(){}).constructor;
Promise.coroutine.addYieldHandler(function(gen) {
    if (gen && (gen instanceof Gen)){ // detect explicit generator
        return Promise.coroutine(gen)();
    }
});

That's for the sake of completeness though :)

like image 188
Benjamin Gruenbaum Avatar answered Nov 15 '22 23:11

Benjamin Gruenbaum