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

Tab Atkins Jr. jackalmage at gmail.com
Fri Apr 19 15:14:14 PDT 2013


On Fri, Apr 19, 2013 at 2:24 PM, Ron Buckton <rbuckton at chronicles.org> wrote:
> 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.

"accept" is just syntax sugar.  If you only provide resolve/reject,
you can get the same behavior as "accept" by just wrapping the value
in a dummy Future before returning it.

The fact that chained futures *only* have resolve/reject semantics
makes this pretty clear.

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

I've also given thought to this, but, even though they're structurally
similar at first glance, the use-cases for ProgressFuture and
EventStream are actually quite different.  A ProgressFuture is still
fundamentally focused on the fulfilled/rejected state, while an
EventStream is fundamentally focused on updates.

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

I'm currently of the opinion that progress updates should probably
automatically bubble through chains.  (I'm currently doing the similar
thing with EventStreams, and making completion bubble through.)

> 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 problem with cancellation, as stated, is that it allows one
consumer to affect the state that another consumer sees.  Right now,
that's not a possibility, which lets you reason about futures much
more easily.  (The fact that you can do this in jQuery's promises, for
example, makes them extremely hard to work with generically.)

As Alex says, creating a Future subclass that's single-listener would
avoid this issue, so cancelling would probably work.

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

Most consumers of Futures won't be using the constructor - they'll
just be handed an already-constructed future for them to listen to.
So, using the constructor as the channel to pass in cancellation info
won't really help. :/

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

I've wiped out that function for now, because I had the semantics of
listen() wrong.  I need to figure out a better way to unlisten.

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

Interesting ideas!

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

Correct, and I expect the same.  (That said, we can probably at least
adopt Promises/A+ adoption semantics, where thenables with behavior
that is anywhere near sane can be automatically converted into
Futures.)

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

Yes, the order of the result array does need to be in the same order
as the input futures.  Good catch.  I'll file this in a new top-level
thread.

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

No, Future.some always passes an array into the reject handler.  No
need to duck-type, unless you're passing the same reject handler to
multiple futures.  If you are, Array.isArray() is reliable.

> 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 expect these kind of conveniences to show up eventually, but likely
in separate specs.  For example, Future.sleep() or Future.delay()
would be defined alongside setTimeout().

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

Yes, Q has some basic support for reconstructing a stack from errors.
This should be explored more fully, because otherwise it's very hard
to use errors for debugging.

~TJ


More information about the es-discuss mailing list