Promise/Future: asynchrony in 'then'

classic Classic list List threaded Threaded
15 messages Options
Reply | Threaded
Open this post in threaded view
|

Promise/Future: asynchrony in 'then'

Claus Reinke
The promises-aplus spec has a note that confuses me

    https://github.com/promises-aplus/promises-spec#notes

    1. 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.

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?

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:
    http://thanpol.as/javascript/promises-a-performance-hits-you-should-be-aware-of/ 
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Juan Ignacio Dopazo
2013/4/30 Claus Reinke <[hidden email]>
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.

Juan

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: Promise/Future: asynchrony in 'then'

Domenic Denicola-2
In reply to this post by Claus Reinke
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

https://github.com/promises-aplus/promises-spec/issues/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 https://github.com/promises-aplus/promises-spec/pull/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).
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Anne van Kesteren
In reply to this post by Juan Ignacio Dopazo
On Tue, Apr 30, 2013 at 5:55 PM, Juan Ignacio Dopazo
<[hidden email]> 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.


--
http://annevankesteren.nl/
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Tab Atkins Jr.
In reply to this post by Claus Reinke
On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <[hidden email]> wrote:

> The promises-aplus spec has a note that confuses me
>
>    https://github.com/promises-aplus/promises-spec#notes
>
>    1. 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:
>
> http://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.

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Alex Russell-4


On Wednesday, May 1, 2013, Tab Atkins Jr. wrote:
On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <<a href="javascript:;" onclick="_e(event, &#39;cvml&#39;, &#39;claus.reinke@talk21.com&#39;)">claus.reinke@...> wrote:
> The promises-aplus spec has a note that confuses me
>
>    https://github.com/promises-aplus/promises-spec#notes
>
>    1. 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.
 
> 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:
>
> http://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.

~TJ
_______________________________________________
es-discuss mailing list
<a href="javascript:;" onclick="_e(event, &#39;cvml&#39;, &#39;es-discuss@mozilla.org&#39;)">es-discuss@...
https://mail.mozilla.org/listinfo/es-discuss

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Tab Atkins Jr.
On Wed, May 1, 2013 at 9:07 AM, Alex Russell <[hidden email]> wrote:

> On Wednesday, May 1, 2013, Tab Atkins Jr. wrote:
>> On Tue, Apr 30, 2013 at 9:43 AM, Claus Reinke <[hidden email]>
>> wrote:
>> > The promises-aplus spec has a note that confuses me
>> >
>> >    https://github.com/promises-aplus/promises-spec#notes
>> >
>> >    1. 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.  ^_^

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Sam L'ecuyer
In reply to this post by Alex Russell-4
>> 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.

-s

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: Promise/Future: asynchrony in 'then'

Domenic Denicola-2
From: Sam L'ecuyer [[hidden email]]

> 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: https://github.com/promises-aplus/promises-spec/pull/104#issuecomment-17290173

_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

RE: Promise/Future: asynchrony in 'then'

medikoo
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:
https://github.com/kriskowal/q/pull/259
https://github.com/tildeio/rsvp.js/issues/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: https://github.com/cujojs/when/issues/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.
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Claus Reinke
In reply to this post by Claus Reinke
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
 
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Tab Atkins Jr.
On Thu, May 2, 2013 at 2:28 PM, Claus Reinke <[hidden email]> 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.

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Claus Reinke
>>>    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
 
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Tab Atkins Jr.
On Fri, May 3, 2013 at 1:53 AM, Claus Reinke <[hidden email]> 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.

~TJ
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss
Reply | Threaded
Open this post in threaded view
|

Re: Promise/Future: asynchrony in 'then'

Claus Reinke
>> 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:

https://github.com/promises-aplus/promises-spec/issues/94#issuecomment-16193265

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
 
_______________________________________________
es-discuss mailing list
[hidden email]
https://mail.mozilla.org/listinfo/es-discuss