The topic of single-frame continuations has been discussed here before,
with the current state of ES.next moving towards generators that are
based on and similar to Mozilla's JS1.7 implementation. Generators are a
powerful addition to the language and welcome (at least by me). However,
I believe that this still leaves a distinct gap in functionality that
forces the majority use case for single-frame continuations to be
handled by libraries. A language feature that OOTB must rely on
libraries to fulfill the most wanted use cases seems like than ideal.
I believe one could separate these single-frame continuations (or
coroutines, looking at it from the perspective of the behavior of the
function) into two categories. There are bottom-up controlled
continuations, where the caller of the coroutine function controls when
the function will resume execution. I think is equivalent to a
generator. Generator functions return an object with an interface for
resuming the execution (and providing values for the continuation) of
the coroutine function.
There are are also top-down controlled continuations. Here coroutine
functions can suspend execution when given an object (typically from one
of the functions it calls) that provides the interface to resume
execution. Resuming execution therefore is controlled by values returned
from callees instead of from the caller. It is worth noting that
bottom-up controllers can be turned into a top-down controller and vice
versa with the use of libraries (one can go either way).
I believe that the overwhelming need that is continually and constantly
expressed and felt in the JS community in terms of handling asynchronous
activity is fundamentally a cry for top-down controlled single-frame
continuations (obviously not always stated in such terms, but that is
the effective need/solution). In terms of an actual code example,
essentially what is desired is to be able to write functions like:
element.onclick = function(){
// suspend execution after doSomethingAsync() to wait for result
var result = <some operator> doSomethingAsync();
// resume and do something else
alert(result);
};
Generators directly solve a problem that is much less significant in
normal JS coding. While it is exciting that generators coupled with
libraries give us a much better tool for asynchronous use cases (the
above can be coded with libraryFunction(function*(){...}), my concern is
that the majority use case is the one that requires libraries rather
than the minority case, and does not promote interoperability.
Now why should we consider something now when previous alternatives to
generators have failed to gain traction? Previous proposals have avoided
a specifying a direct interface on top-down objects to leave the door
open for different possible interfaces for resuming executions, or
different possible "promise" styles. We have wisely deferred to
libraries when different possible approaches have yet to be explored
within the JS community. A couple years ago there were numerous
approaches being explored. However to truly follow through with this
strategy we should then proceed with language design when convergence
does in fact take place. A couple years later, I believe the landscape
has dramatically changed, and we indeed do have significant convergence
on a promise API with the "thenable" interface. From Dojo, to jQuery, to
several server side libraries, and apparently even Windows 8's JS APIs
(from what I understand) all share an intersection of APIs that include
a then() method as a method to define a promise and register a callback
for when a promise is fulfilled (or fails). This is an unusual level of
convergence for a JS community that is so diverse. I believe this gives
evidence of well substantiated and tested interface that can be used for
top-controlled single-frame continuations that can easily be specified,
understood and used by developers.
My proposal is to allow the use of the "yield" keyword in standard
functions (not just generator function*'s) with the following semantics:
The "yield" operator is prefix operator that takes a single operand
(variant of AssignmentExpression, just as within generator function*s).
When a "yield" operator is encountered in the execution of a standard
function (not a generator), the operand value is examined. If the value
is an object with a "then" property that is a function (the object is
AKA "promise"), the execution will suspend, preserving the context for
when the execution is resumed. The operand's "then" function will be
called with a "resume" function as the first argument, and a "fail"
function as the second argument. If and when the "resume" function is
called, execution of the suspended function will resume. The first
argument of the call to the "resume" function will be provided as the
result of the evaluation yield operator within the resumed execution. If
the "fail" function is called, execution will be resumed with the value
of argument to the fail function being immediately thrown from the yield
expression. Once the "resume" function or the "fail" function is called,
any subsequent calls to either function should result in an error. The
mechanics of context preservation should otherwise follow the same
principles as generators.
When the executing function is suspended, it will return a new object
with a "then" method. The object's then() method may be called and the
first argument can be a callback that will be called when the executed
function eventually is finished (encounters a return statement or end of
the body after resuming execution). The return value of the function
will be provided as the first argument to the callback. If the executed
function eventually throws an error, the second argument provided to the
then() method will be called.
If the value is not an object with a "then" property that is a function,
the operand value is the immediate result of the evaluation of the
"yield" expression and execution is not suspended.
Here is a simple example of usage:
"use strict"
function delay(ms){
// a function that will yield for the given milliseconds
yield {
then: function(resume){
setTimeout(resume, ms);
}
}
}
function fadeOut(element){
// fade out by yielding for 20ms in each loop
for(var i = 0; i < 100; i++){
element.style.opacity = i / 100;
yield delay(20);
}
}
One of particular compelling aspects of this proposal is how elegantly
it works with existing code. Because jQuery (and others) follow the same
interface, we could immediately write the following with the existing
jQuery 1.5+ library:
"use strict"
function showData(){
// make a request, asynchronously wait for the response, put the
result in an element
element.innerHTML = yield $.ajax({url:"target"});
}
Obviously one could choose a different keyword to use. I'd imagine
"await" is one alternative. The drawback of "yield" is that it does
behave differently than a yield in a generator. However, generators
behave quite differently anyway, and top-controlled "yield" shares a
very important commonality. Using one keyword means there is only a
single operator that can result in a suspension of execution within a
function body, making easier to spot such occurrences and look for
possible points where certain variants should not be anticipated. Of
course it is also nice to avoid proliferation of keywords and
introducing new previously unreserved keywords.
There are also have been suggestions about potentially have language
support for queuing different operations on to promises (gets, puts, and
posts). I don't think proposal precludes such possibilities (it only
precludes the possibility of opaque promises that have no interface, but
the majority of devs I have talked to are pretty opposed to that idea,
so that doesn't seem like likely possibility).
Thanks,
Kris
_______________________________________________
es-discuss mailing list
[email protected]
https://mail.mozilla.org/listinfo/es-discuss