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

Kevin Gadd kevin.gadd at gmail.com
Fri Apr 19 15:35:07 PDT 2013


My solution for cancellation has been to allow cancellation notifications
to be bidirectional - that is, when you subscribe to completion
notifications on a Future, you can also subscribe to cancellation
notifications. Then it's possible to cancel a given future without breaking
any other listeners (as long as they subscribed to cancellation
notifications if they care about cancellation). Has that been considered? I
can see how it might be too finicky for the average developer; losing out
on cancellation really sucks though.

In particular it feels more important to have explicit cancellation built
into the object representing work if you can in JS, since there's no way to
lean on the garbage collector to cancel work - in environments like Python
you can make cancellation implicit by doing it when the Future representing
the work is collected, but in JS that's impossible, so having an explicit
way to dispose of a future is valuable, even if in many cases the
cancellation doesn't do anything. It's also particularly good in terms of
encapsulation - if there's a general cancellation mechanism that is
well-factored, you can just universally make a habit of cancelling unneeded
futures, and any backend implementations that support cancellation will
automatically get told to cancel and save cycles/bandwidth. It means that
you don't have to go add cancellation in 'after the fact' when the source
of a Future changes from a local buffer to a network operation, or remove
cancellation when you replace a network operation with a cache.

Any kind of task scheduler like dherman's task.js can easily leverage this
to automatically cancel any task represented by a cancelled Future, and in
particular, task schedulers can propagate cancellation, by cancelling any
of the Futures a task is waiting on when the task is cancelled. This has a
very desirable property of allowing you to cancel a huge, amorphous blob of
pending work when it becomes unnecessary by simply cancelling the root -
for example in one application I worked on, we kicked off a task to
represent each avatar in a 3D scene that was responsible for loading the
avatar's textures, meshes, etc. If the user left the scene before the
avatar was fully loaded, all we had to do was cancel the task and any
pending texture loads or network requests automatically stopped. Getting
that right by hand would have been much more difficult, and we wouldn't
have necessarily known to build cancellation explicitly into that API when
we started.


On Fri, Apr 19, 2013 at 3:14 PM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

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



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


More information about the es-discuss mailing list