Futures (was: Request for JSON-LD API review)

Ron Buckton rbuckton at chronicles.org
Fri Apr 19 14:24:43 PDT 2013


I’ll preface this with a disclaimer that I’m not directly involved with any of the standards discussions, TypeScript, or IE at Microsoft, but rather am expressing my interest as a producer and consumer of Promises/Futures while building cloud applications. I’ve published a few variations of Promise/Future libraries for JavaScript at http://blogs.msdn.com/b/rbuckton and a lot of my experience is biased towards previously having leveraged Task-based asynchrony in C#.

Despite my API proposal leaning towards the term Promise, I’ll use the term Future instead to align with the proposal for DOM when speaking about Promises or Futures in the general sense.

My version of the PromiseResolver provides resolve/reject methods and does not include an ‘accept’ method. My understanding of this is that FutureResolver#accept resolves the value explicitly, while FutureResolver#resolve hooks the Future#done method of the value if it is a future so that Future(A) for Future(B) is eventually resolved as B, not Future(B). I’m not sure I understand all the use cases for accept over resolve at this point, but have always preferred the resolve approach in all of my actual uses so far.

Progress notifications are a bit of a mixed bag for me. My first implementations of Future didn’t have them, but I had a number of instances in the C# world where I needed a way to update the Future consumer for the benefit of UI notifications. The recent discussion on EventStreams has me wondering if progress notifications could serve a similar purpose for Futures, where a progress notification could be triggered for each instance of an event in the stream, where resolve is triggered for single use events (like DOMContentLoaded), or when the event producer is signaling that it has concluded processing.

With Progress notifications relegated to a subclass, would chained Futures also be ProgressFuture instances? The benefit of having progress as an optional member of the Future class is that a chained Future could also enlist in progress notifications, but that is less of a concern if the Future created by a .then from a ProgressFuture is itself a ProgressFuture.

Not having progress can be mitigated somewhat by passing in a progress notification callback to the function that creates the Future, but chained descendants would not be aware of the progress and would have a more complicated task to write code that properly accepts progress handlers, and then we’re getting back to the callback/errback continuation passing that Futures are partially designed to replace.

Cancellation was an attempt to support something akin to Cooperative Cancellation as it exists in .NET’s implementation, as well as a means to ‘unlisten’ or forget a Future if you no longer need its value. In the API proposal, by default cancel would only essentially remove a Future (and therefore its chained descendants) from receiving resolve/reject/progress signals from its antecedent. Cancellation also would allow the ability to prevent the resolve/reject/progress callbacks from executing in a later turn of the dispatcher to prevent the execution of now unneeded code.  It can also be used to abort a XHR request or shut down a WebWorker.

The second callback in the Promise constructor would be a means to provide user supplied cancellation logic, such a updating the UI in response to a cancelled pending operation.  I debated on whether it should be possible to also cancel the antecedent tasks from a chained descendent, and it is a very tentative part of the API.

In the .NET world, I would use a CancelationTokenSource and CancellationToken to provide cancellation, which serves several purposes. One is the ability to prevent the execution of the background Task before it starts (which is provided by adding Promise#cancel()). Second is the ability to perform some kind of user-defined cleanup logic in the event of cancellation (e.g. detach event handlers, abort an XHR, notify the UI, etc.). The third is the ability to track cancellation when in a background thread that might be running in a loop, however with the possible exception of Web Workers, this is unlikely to be required in traditional JavaScript programs that are single threaded and rely on a dispatcher/event-loop and don’t have the traditional concept of a Thread. CTS also allows the ability to aggregate multiple cancellation tokens when waiting on multiple parallel tasks, which is even less likely in JavaScript/ES.

Promise#cancel() in this respect can have an effect similar to EventStream#unlisten in that proposal.

That being said, a “CancellationToken” could be implemented by passing in another Future to the function that generates the Future you care about. Resolving the “cancellation” future could be used to abort an XHR, but not to cancel a task that is still waiting to be executed on the dispatcher/event-loop, as the .then() would likely execute in a different turn, unless it could be explicitly marked as synchronous.

The options argument provides additional optional named parameters for the then/done/catch/progress continuations that in effect make the “synchronous” flag in the DOMFutures something that the user can control. This is similar to the TaskContinuationOptions.ExecuteSynchronously enum value in .NET which can be used to optimize some continuations to execute synchronously when its antecedent is resolved or rejected to reduce the need to wait for another turn of the dispatcher/event-loop. This optimization is primarily defined for small function bodies to reduce overhead, and could be used to make cancellation-by-future more effective.

The reason options is expected to be an object/object literal is that this can be extended to add additional control over the resulting continuation. This could include the ability to prevent cancellation (in the event .cancel is supported with the antecedents argument), or the ability to only signal chained descendants if a future is rejected and not to forward resolve to those descendants. This also allows for future additions to the options in later versions without breaking consumers. In this vein, it could be useful to have an options argument for the Future constructor as well, although I haven’t yet had an occasion to need one yet.

Finally, the additional API definitions are convenience APIs for certain scenarios. By default, I expect both Promise.resolve and PromiseResolver#resolve to only hook the resolve/reject of a Promise from the same library. Calling Promise as a function (or adding a Promise.of static method) might be the only Promise ‘interop' to userland Future libraries, though I would almost prefer that no ‘interop’ between libraries for a DOM or ES version to exist, but rather would require explicitly creating a new Future and using its resolver to interoperate with the userland promise.

The Promise.any, Promise.every, and Promise.some methods are very similar to what is in DOMFutures, except that the current version of the DOMFutures spec leaves a few things unspecified that could be problematic for end users. According to the spec for Future.every, order of the resolved values is arbitrary, based on the order that the provided Futures are resolved. As a consumer of Future.every, the Array of resolved values should be in the same order as the futures that were provided to the method, to be able to distinguish which value belongs to which future.  This may or may not be in the polyfill, but it is not explicitly (or at least clearly) specified in the DOMFutures spec. The same can be said for the Array of errors in the Future.some API definition.

I added AggregateError as a tentative Error object as a means to provide a single Error object to use as the value for the reject handler, and have considered wrapping all non Error values passed to the reject method on the resolver into an Error object to set expectations for the consumer. That way, the argument to the reject callback is always recognizable as an Error, and it can be easier to test the argument to provide appropriate handling. For instance, without Error wrapping or AggregateError, I would have to result to duck typing or Array.isArray to determine whether the errors provided are the result of a single error or multiple errors from a call to Future.some. This is, again, inspired by the .NET AggregateException, though I would likely send the single underlying Error if the AggregateError would only contain a single error.

The remaining API’s are designed to help support await-style asynchronous development as possibly afforded by generators or any future addition of something like “await” into the language. To that end, static methods like Promise.yield() and Promise.sleep() can help to let the dispatcher/event-loop do other work in the middle of a long-running async function, or to pause for a period of time before continuing such as with animation. Promise.delay() is similar to sleep, but resolves with a value.

Promise.run() is close to setImmediate, where the result is the future value of the callback. In this case, Promise#cancel() is then effectively a call to clearImmediate. In a similar fashion, Promise.start() is roughly equivalent to setTimeout with its Promise#cancel() then synonymous with clearTimeout.

I’m not strongly tied to having progress, cancel, or the synchronous option, but do find that they provide a level of flexibility. Subclassing Future to provide this could make sense, but again I am concerned about ensuring the subclass prototype is somehow reused for chained dependents so that you don’t lose your .progress or .cancel if you do a .then before you return. The .yield/.sleep/.delay convenience methods are much more useful with yield or await.

I can understand Luke’s concern around the state properties, the only one I might push back on might be PromiseResolver#wasCanceled if Promise#cancel were to be supported to be able to test for cancellation if the future might be resolved in a later turn than it was created (such as in the onload event listener for an XHR). The properties on Promise itself are much less necessary and I’m not strongly tied to them.

One last thing not mentioned in my proposal, nor the DOMFutures spec, is dealing with Error#stack with respect to Futures or async methods. Since ES has no rethrow concept, the only way for a reject handler to pass the error to chained descendants is to throw the exception. This can then possibly negatively effect the content of Error#stack and can complicate debugging futures. I am still reading through the issues list for DOMFutures, so I apologize in advance if this is a topic that has already been covered.

I sincerely look forward to a standard implementation of Futures, and truly hope this can become part of ES. Some of the proposals for ES6 and later could likely benefit from Futures. The current proposal for module Loaders already is leaning towards both Node/CPS-like callback/errback arguments for Loader#load as well as something very Future-like in its argument to Loader#fetch (at least as far as of what I have been able to find online). It seems to me that both methods would be better served by Futures. Object.observe could be served by something like the EventStreams proposal as well.

Best regards,
Ron

Sent from Windows Mail

From: Alex Russell
Sent: ‎Friday‎, ‎April‎ ‎19‎, ‎2013 ‎4‎:‎58‎ ‎AM
To: Ron Buckton
Cc: Anne van Kesteren, Mark S. Miller, es-discuss, public-script-coord at w3.org, Markus Lanthaler, Douglas Crockford, Norbert Lindenberg, Luke Hoban, Tony Ross, Adrian Bateman

Hi Ron,

Comments inline.

On Wed, Apr 17, 2013 at 7:35 PM, Ron Buckton <Ron.Buckton at microsoft.com<mailto:Ron.Buckton at microsoft.com>> wrote:
As someone who has been interested in Promises/Futures in JavaScript for a number of years, I'd like to throw in my $0.02 regarding a proposed API for Promises/Futures for thoughts:

https://gist.github.com/rbuckton/5406451

My apologies in advance as the API definitions are written using TypeScript and not Web IDL.


There's a lot of API in here. If you give the DOMFutures github repo a look, you'll see that we considered some of them: https://github.com/slightlyoff/DOMFuture/

In particular, progress was explicitly written out of the contract of the base class so that subclasses that need it can mix it back in without burdening everyone else with perhaps nonsensical methods. See: https://github.com/slightlyoff/DOMFuture/blob/master/ProgressFuture.idl

As for the state variables, we've removed them in the most recent version to avoid potential for "cheating" (as Luke Hoban described it). I'm also not entirely sure I understand why the capability to cancel is being vended to all consumers of a Future. We explicitly disallow that in the current design to prevent the potential for multiple users of a Promise/Future stepping on each other. The thought with the current design is that if you have an interface that needs to vend cancelation, you should at it in a subclass of Future.

Your constructor signature looks similar (but not the same) as the one we ended up with, and there appear to be many convenience static methods on Promise. How strongly do you feel about them?

>From a design standpoint the only thing that jumps out at me as being very strange is the "synchronous" option for .then() and .done(). What is it meant to do?

-----Original Message-----
From: Anne van Kesteren [mailto:annevk at annevk.nl<mailto:annevk at annevk.nl>]
Sent: Wednesday, April 17, 2013 8:46 AM
To: Mark S. Miller
Cc: public-script-coord at w3.org<mailto:public-script-coord at w3.org>; Norbert Lindenberg; Markus Lanthaler; Douglas Crockford; es-discuss
Subject: Re: Futures (was: Request for JSON-LD API review)

> On Wed, Apr 17, 2013 at 8:29 AM, Mark S. Miller <erights at google.com<mailto:erights at google.com>> wrote:
>> The main argument I've heard for proceeding with w3c/DOMFutures
>> rather than tc39/promises is that the DOM can't wait for tc39 to get
>> around to standardizing promises in ES7. But we should have our eyes
>> open to the consequences. As Crockford says (paraphrasing Knuth)
>> "Premature standardization is the root of all evil." The likely
>> result of DOMFuture proceeding in this way is that it will be wrong,
>> ES7 will be stuck with it and mostly unable to fix it, and we will
>> all be stuck with the consequences for a very very long time.
>>
>> As with Object.observe, if the need for promises is that urgent, it
>> needs to be on an accelerated track with the JavaScript context -- as
>> it already de facto is at promises/A+. It should not be needlessly
>> tied to the browser or to w3c.

I don't find the whole who owns what discussions very interesting to be honest. If it was up to me JavaScript would just be part of the W3C and we would not have to deal with that layer of distraction.

In any event, you can take the specification and improve on it elsewhere if you so desire. It is in the public domain for a reason.
You can also provide technical feedback as to what exactly is evil.
Saying "stop doing this" and implying you're somehow the superior forum to the other party is not helpful and has certainly not helped in the past.


--
http://annevankesteren.nl/

_______________________________________________
es-discuss mailing list
es-discuss at mozilla.org<mailto:es-discuss at mozilla.org>
https://mail.mozilla.org/listinfo/es-discuss

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20130419/ced7673a/attachment-0001.html>


More information about the es-discuss mailing list