Promise/Future: asynchrony in 'then'
2013/4/30 Claus Reinke <claus.reinke at talk21.com>
I have not yet been able to decide whether DOMFuture has a similar provision, or how this note is meant to be interpreted.
I think it is covered by this language in the accept() algorithm:
Otherwise, the synchronous flag is unset, queue a task to process future's resolve callbacks with value
"queue a task" points to text in the Web Apps spec regarding the event loop.
This is actually up for clarification in the upcoming 1.1 version of the spec. The important invariant is that the function execution stack be empty of user code at the time onFulfilled
and onRejected
are called. The current phrasing does not suffice for this, as pointed out in
promises-aplus/promises-spec#100
The current phrasing is just an (insufficient) means of working around the fact that JS doesn't specify an event loop; an attempt at being clever.
Discussion is ongoing at promises-aplus/promises-spec#104
We seem to be converging around something more like the invariant I spoke of above, as prompted by Mark Miller; e.g. "onFulfilled
or onRejected
must not be called when the stack contains any user code." But this requires defining user code, which is tricky---it includes [native code]
, but also base code from the platform (e.g. Node.js's built-in modules), and parts of the promise implementation itself (e.g. the internal trampolines used by Q and when among others, or the setImmediate polyfill many fall back on).
On Tue, Apr 30, 2013 at 5:55 PM, Juan Ignacio Dopazo <dopazo.juan at gmail.com> wrote:
I think it is covered by this language in the accept() algorithm:
Otherwise, the synchronous flag is unset, queue a task to process future's resolve callbacks with value
"queue a task" points to text in the Web Apps spec regarding the event loop.
Right. We might want to run this as a microtask instead maybe, but I
think a task similar to what setTimeout(..., 0)
does is fine.
On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <claus.reinke at talk21.com> wrote:
The promises-aplus spec has a note that confuses me
promises-aplus/promises-spec#notes
- In practical terms, an implementation must use a mechanism such as setTimeout, setImmediate, or process.nextTick to ensure that onFulfilled and onRejected are not invoked in the same turn of the event loop as the call to then to which they are passed.
I have not yet been able to decide whether DOMFuture has a similar provision, or how this note is meant to be interpreted.
Juan already pointed out the "queue a task" language, so this is answered.
The aspect that worries me is that this note is attached not to the creation of promises but to the definition of 'then'. Is that because of the implicit return lifting (if 'then' callbacks do not return promises, wrap the return in a new promise), or is there something else going on?
Yes. The promise itself may be fulfilled synchronously - the resolver callback is called immediately, and can immediately call "r.accept(val)" if it wants. The important part is that chained callbacks (through .then(), .done(), etc.) be handled async.
As long as the 'then' callbacks return Promises, the idea of resolved Promise creation as left and right identity of 'then'
Promise.of(value).then(cb) = cb(value) promise.then(Promise.of) = promise
would seem to require no additional delays introduced by 'then' (promise creation decides semantics/delays, 'then' only passes on intermediate results).
Could someone please clear up this aspect? How is that note meant to be interpreted, and do other Promise/Future specs have similar provisions?
Claus
PS. Prompted by this blog post:
thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of
The reason you want to maintain asynchrony, even when it's possible to go ahead and run callbacks async, is so that the operation of futures is predictable. It doesn't matter (and it's unobservable) whether a future is pending or fulfilled - you just attach a callback to it, and it'll run no earlier than the next event-loop tick, so the rest of the code in your function can depend on the fact that it hasn't run yet. If it sometimes runs synchronously, then you can't depend on that - the rest of the code in your function may be assuming that certain outside variables don't change, but the promise might tweak them synchronously, or might leave them alone until it runs async.
On Wednesday, May 1, 2013, Tab Atkins Jr. wrote:
On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <claus.reinke at talk21.com<javascript:;>> wrote:
The promises-aplus spec has a note that confuses me
promises-aplus/promises-spec#notes
- In practical terms, an implementation must use a mechanism such as setTimeout, setImmediate, or process.nextTick to ensure that onFulfilled and onRejected are not invoked in the same turn of the event loop as the call to then to which they are passed.
I have not yet been able to decide whether DOMFuture has a similar provision, or how this note is meant to be interpreted.
Juan already pointed out the "queue a task" language, so this is answered.
This is far too glib. The spec may very well be wrong on this point. The design goal isn't to require a full yeild of the event loop, but instead to force async code flow -- that means that resolving and calling back should be able to happen at "end of microtask"; the same timing as Object.observe() callbacks.
On Wed, May 1, 2013 at 9:07 AM, Alex Russell <slightlyoff at google.com> wrote:
On Wednesday, May 1, 2013, Tab Atkins Jr. wrote:
On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <claus.reinke at talk21.com> wrote:
The promises-aplus spec has a note that confuses me
promises-aplus/promises-spec#notes
- In practical terms, an implementation must use a mechanism such as setTimeout, setImmediate, or process.nextTick to ensure that onFulfilled and onRejected are not invoked in the same turn of the event loop as the call to then to which they are passed.
I have not yet been able to decide whether DOMFuture has a similar provision, or how this note is meant to be interpreted.
Juan already pointed out the "queue a task" language, so this is answered.
This is far too glib. The spec may very well be wrong on this point. The design goal isn't to require a full yeild of the event loop, but instead to force async code flow -- that means that resolving and calling back should be able to happen at "end of microtask"; the same timing as Object.observe() callbacks.
Possibly true, but those are details. The context of my response was someone asking about whether DOMFutures described asynchrony at all. The spec does mandate asynchrony, even if it might be the wrong kind of asynchrony. ^_^
Juan already pointed out the "queue a task" language, so this is answered.
This is far too glib. The spec may very well be wrong on this point. The design goal isn't to require a full yeild of the event loop, but instead to force async code flow -- that means that resolving and calling back should be able to happen at "end of microtask"; the same timing as Object.observe() callbacks.
Promises/A says has an open issue on whether callback handlers should be put on the event queue or executed immediately.
Promises/A+ & Promises/B both specify that callbacks may not be invoked in the same turn of the event loop.
According to the harmony:observe doc, it was suggested that "Schedule change events to be delivered asynchronously “at the end of the turn”".
These may all be wrong, but all seem to be hitting at the fact that "in a new turn" is the easiest way to force asynchronicity. Maybe we need to specify that all callbacks must be invoked asynchronously, regardless of which turn in the event loop they occur.
From: Sam L'ecuyer [sam at cateches.is]
These may all be wrong, but all seem to be hitting at the fact that "in a new turn" is the easiest way to force asynchronicity. Maybe we need to specify that all callbacks must be invoked asynchronously, regardless of which turn in the event loop they occur.
We need to figure out what invariants we are enforcing, and codify them with code samples and tests. Then we can figure out the best way to specify that with words; whether the exact mechanism is microtasks or macrotasks doesn't matter, as long as the invariants are preserved.
So far we have three example invariants in Promises/A+ land; more welcome: promises-aplus/promises-spec#104
I think eventual synchronicity of then
is just theoretical problem and in
practice we face much bigger issues when we force asynchronicity.
If we want to have "fast" asynchronicity, then we enter the problem of overloaded recurrence of next tick calls. It was already exposed by popular libraries: kriskowal/q#259, tildeio/rsvp.js#66
If we do it with setTimeout or setImmediate, then performance is significantly affected, and that diminishes the benefit of why we're actually using asynchronous calls.
Promises while being aid for asynchronicity should not introduce any extra asynchronicity on it's own. Any optional next-tick resolution should be decided by user of library and not by library itself. If library forces it by design then we enter the world of issues like that one: cujojs/when#135
I'm using promise library which few versions back moved away from forced next-tick resolution (mainly for performance reasons), and I didn't register any issues caused by that change. It actually confirmed me, that it's the way it should be.
-- View this message in context: mozilla.6506.n7.nabble.com/Promise-Future-asynchrony-in-then-tp278846p279080.html Sent from the Mozilla - ECMAScript 4 discussion mailing list archive at Nabble.com.
Thanks for the various references and explanations - it took me a while to follow them. So, while discussion is still ongoing on the details (both of how to spec and what to spec), all specs seem to agree on trying to force asynchrony, and on doing something in 'then' to achieve this.
I suspect that at least the latter part is wrong - at least it is in conflict with decades of design and coding experience with general monadic APIs, especially with the idea of providing one effect-free form of creation that is left and right identity to composition:
Promise.of(value).then(cb) = cb(value) promise.then(Promise.of) = promise
My interpretation of these laws for promises is that attaching a callback to a resolved promise should execute that callback synchronously (though the callback itself may create an asynchronous promise, introducing its own delays).
Similarly, a callback creating a resolved promise from a future result should not add further delays beyond those of the original future-result-creating promise.
This does not affect design decisions about promise resolution, so the motivating examples could still work.
One of the examples in the linked threads was roughly:
{ promise, resolve } = ...
promise.then( r => Promise.of( console.log( r ) ) );
console.log(1);
resolve(2);
console.log(3);
expecting output order 1 3 2. This would still be possible if resolve itself was asynchronous (queuing the callbacks for the next or end of current turn instead of the current one) - no need to introduce asynchrony in 'then', it seems, not even in the implicit result lifting.
Explicitly providing for both synchronous and asynchronous promises also seems more predictable and performance-tunable than leaving next-ticking to implementation optimization efforts.
At least some of the alternatives discussed also violate the third law of monadic interfaces, associativity of composition:
promise.then( cb1 ).then( cb2 )
=
promise.then( r=>cb1( r ).then( cb2 ) )
if one was to add nextTicks in 'then' (which I think is a bad idea anyway), then cb1 and cb2 should not be queued for the same next turn. Which would lead to the accumulation of delays that were reported for some promise implementations, just for using monadic callback composition.
Associativity is less of a problem for alternatives that propose to move resolved promise callback execution to the end of the current turn (with or without some means to protect separate execution threads from each other). Though that leaves the question of starving other queued tasks by continuing to extend the current turn with presumably asynchronous tasks. Again, having explicit control over synchronous vs asynchronous resolution of intermediate promises would help with tuning the queuing.
Claus
On Thu, May 2, 2013 at 2:28 PM, Claus Reinke <claus.reinke at talk21.com> wrote:
Promise.of(value).then(cb) = cb(value) promise.then(Promise.of) = promise
My interpretation of these laws for promises is that attaching a callback to a resolved promise should execute that callback synchronously (though the callback itself may create an asynchronous promise, introducing its own delays).
It's not that the callback "may" create an async promise: it must create an async promise, if you want to reason about it in terms of the monad laws. The cb must have the signature "a -> Mb", where M in
this case is Promise. If cb returns a non-promise value, then you're not following the monad laws, and you can't reason about monadic behavior.
Assuming it does follow the monad laws properly, then the return value of cb is always accessible in the next tick only, regardless of whether it runs synchronously or not. If you're depending on side-effects, you're already stepping outside of what the monad abstraction is meant to contain, so slight differences like that are acceptable.
Promise.of(value).then(cb) = cb(value) promise.then(Promise.of) = promise
My interpretation of these laws for promises is that attaching a callback to a resolved promise should execute that callback synchronously (though the callback itself may create an asynchronous promise, introducing its own delays).
It's not that the callback "may" create an async promise: it must create an async promise, if you want to reason about it in terms of the monad laws. The cb must have the signature "a -> Mb", where M in this case is Promise. If cb returns a non-promise value, then you're not following the monad laws, and you can't reason about monadic behavior.
We need cb :: a -> Promise<b> in order to avoid the non-monadic
overloads of 'then' in current promise specs but I was referring to the choice of such a callback returning a resolved-now promise (Promise.of) or a resolve-later promise (via some asynchronous operation like nextTick, ajax, ...).
Assuming it does follow the monad laws properly, then the return value of cb is always accessible in the next tick only, regardless of whether it runs synchronously or not.
That part I wouldn't be so sure about: in all monads, the .of equivalent is effect-free (in an IO monad, it does no IO; in a non-determinism monad, it is deterministic; in a failure/exception monad, it does not fail; in a count-steps monad, it doesn't count).
If you look at those identity laws at the top again, you'll see that Promise.of cannot introduce a delay for these laws to work out (otherwise, the left- and right-hand sides would have different numbers of ticks/turns).
Almost all monads have other monad constructors that do have effects (do IO, add non-determinism, throw an exception, ...). It is just that the monad laws are about the effect-free part only.
At least that is my current reading of the situation;-) Claus
On Fri, May 3, 2013 at 1:53 AM, Claus Reinke <claus.reinke at talk21.com> wrote:
That part I wouldn't be so sure about: in all monads, the .of equivalent is effect-free (in an IO monad, it does no IO; in a non-determinism monad, it is deterministic; in a failure/exception monad, it does not fail; in a count-steps monad, it doesn't count). If you look at those identity laws at the top again, you'll see that Promise.of cannot introduce a delay for these laws to work out (otherwise, the left- and right-hand sides would have different numbers of ticks/turns).
As I said, the number of ticks is unobservable if you're writing effect-free code.
If you're not writing effect-free code, then as I said before, keeping the number of ticks the same regardless of the state of the promise when you call .then() on it is important for consistency, so it's easy to reason about how your code will run.
That part I wouldn't be so sure about: in all monads, the .of equivalent is effect-free (in an IO monad, it does no IO; in a non-determinism monad, it is deterministic; in a failure/exception monad, it does not fail; in a count-steps monad, it doesn't count). If you look at those identity laws at the top again, you'll see that Promise.of cannot introduce a delay for these laws to work out (otherwise, the left- and right-hand sides would have different numbers of ticks/turns).
As I said, the number of ticks is unobservable if you're writing effect-free code.
In my use of the the term above, the "effect" of the Promise monad would be to provide a value "maybe now, maybe later", and an "effect-free" '.of' would be an already resolved Promise ("value available now").
Adding ticks in operations that should allow for "effect-free" passing of intermediate results is observable in slow-downs. That was the topic of the blog post that got me to look into this in the first place (performance issues with promise implementations, see thread opening message).
The point of insisting that promises implement a monadic interface is that promises can reuse abstractions built for monads - that also means that passing intermediate values around should not cause additional delays. For instance, in the 'liftA2' example from one of the issue tracker threads:
promises-aplus/promises-spec#94
there are several occurrences of '.then', via 'map' and 'ap', that should not delay the result by several additional turns - the only asynchrony in using 'liftA2' over promises should come from the promise parameters and possibly from the callback parameter.
However, you seem to be referring to side-effects instead (effects beyond returning a value in an expression, beyond the specified effect of a given monad).
Side-effect-free code is difficult to write in JS - I would be surprised if most promise implementations were not full of side-effects (internal queues, shared pipelines, resolution). Also, so many examples of using promises involve side-effects that this seems to count as an established practice.
Which means that the additional code queuing will also be observable in code reorderings, not just delays. Which is, indeed, the rationale for attempting to add delays in a normalized fashion, as you state below:
If you're not writing effect-free code, then as I said before, keeping the number of ticks the same regardless of the state of the promise when you call .then() on it is important for consistency, so it's easy to reason about how your code will run.
Given that many JS APIs still are heavily side-effect biased, we'll need to take that into account. And in that world, adding delays in parts of the promise API that should implement the common monadic interface is very much observable, and will cause code written against this common interface to behave differently when run over a promise than when run over another monad.
Claus
The promises-aplus spec has a note that confuses me promises-aplus/promises-spec#notes
I have not yet been able to decide whether DOMFuture has a similar provision, or how this note is meant to be interpreted.
The aspect that worries me is that this note is attached not to the creation of promises but to the definition of
then
. Is that because of the implicit return lifting (ifthen
callbacks do not return promises, wrap the return in a new promise), or is there something else going on?As long as the
then
callbacks return Promises, the idea of resolved Promise creation as left and right identity ofthen
Promise.of(value).then(cb) = cb(value) promise.then(Promise.of) = promise
would seem to require no additional delays introduced by
then
(promise creation decides semantics/delays,then
only passes on intermediate results).Could someone please clear up this aspect? How is that note meant to be interpreted, and do other Promise/Future specs have similar provisions?
PS. Prompted by this blog post: thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of