Introduction of promise.return() and promise.throw() similar to generator.return() and generator.throw()
Look up the "revealing constructor pattern". There's a reason why only the creator can resolve a promise imperatively.
Yes, it's good note, that it can be "too public" and may be some one would
like to prevent external "intrusion" into it's "private state", but it can
be easily solved by the means of a wrappers, that wraps some private
target and delegate to it only "safe calls", that will not
interrupt-it/cancel-it, it can look like
privateSattePromise.preventAbnormalCompletion() - which returns some
wrapped promise with more "safe" behavior, then this promise can be shared
with "wider audience". But from the opposite side - if somebody make some
internal logic, and do not intend to return that internal objects
"outside", why he should complicate his life so much (and do not "trust
himself" if both methods - which produces some promise and consumes that
promise - are private in some place)? As an example of such kind of
wrappers I can offer you java(Collections#unmodifiableList).
It's conception is very simple - it takes some mutable list and wraps it by
some wrapper which prevents modification, as a result you'll obtain some
"semi-immutable" list, meaning that you will not be able modify it through
that wrapped instance, but still can do some changes over initial
collection, and that changes will be quite visible through the wrapped one.
So from my point of view, it can be 2-wo approaches - always expose that
"external intrusion api", and the explicitly "hide" that api by the means
of some promise.giveMeSafeWrapper()-like api. Or an other option, just
do not expose that api by default, and say that if you want to create that
"extra public" promise you should use some subclass of regular promise,
like extraPublicPromise = new Promice.ExtraPublic(executor), and then
ofcourse, it would be more explicit, which stuff is internal, and should be
wrapped before giving it somewhere outside, and knowing that all the rest
is "safe" in terms of "external intrusion".
But denying that capability at all, being to lazy for writing
.giveMeSafeWrapper()-like calls is also not very good choice (as for
me). (By the way, approach that was described in
the-revealing-constructor-pattern#historical-origins doesn't looks
"so insane". Considering deferred = Q.defer(); and p = deferred.promise;, I see that there are some public part and private part
of that object. And if they would for example share most of api on both of them,
but private instance has "alittle bit more" available api for control, then
in would be not so bad idea. Again, assuming that using private
instance for task completion should NOT be "normal api", but as some sort of
"abnormal completion api" it can be quite useful).
If you really need an object that you describe then you use a function like this to create them:
function deferred(executor=() => {}) {
let _resolve
let _reject
let promise = new Promise((resolve, reject) => {
_resolve = resolve
_reject = reject
})
promise.return = _resolve
promise.throw = _reject
return promise
}
and you can use it as such:
const p = deferred(resolve => setTimeout(resolve, 1000, "Hello, world!")
p.then(result => console.log(result)) // Will display Interrupt not
Hello, world!
// resolve early, no need for preventDefault as only first call to
resolve is considered
p.return("Interrupted")
Something to be aware of is generator return/throw is not about
Promises, its about sending values to and from a generator to allow
two way communication with a execution engine (e.g. creating
applications which can be ported to different environments)
function* app() {
yield ['changeBackground', 'green']
yield ['wait', 1000 /* milliseconds */]
yield ['changeBackground', 'aquamarine' /* missplet */]
}
function engine() {
const iter = app()
function step() {
const { value, done } = app.next()
if (done) {
return // Nothing left to do
}
else {
if (value[0] === 'changeBackground') {
if (!colors.includes(value[1])) {
iter.throw(new Error("Color not found"))
}
// ...etc
}
}
In generators you can respond to values, errors, and returns created from the outside, even try-finally isn't something special for generators, regular functions can skip return with try-finally too e.g.:
function foo() {
try {
return "Hello"
} finally {
return "Goodbye"
}
}
console.log(foo()) // Goodbye because return "Hello" was overridden by finally
Promises aren't generators, async functions aren't either, although
a polyfill can use generators to implement correct async functions by
replacing await with yield and wrapping them with a spawn
function, however thats a polyfill detail, and the polyfill almost
certainly won't replace await foo with yield AwaitResult(foo)
because that's trivially hand-able with promises themselves (see the
officially suggested polyfill:
tc39.github.io/ecmascript-asyncawait/#desugaring).
Generally you won't need anything like Q.deferred, you can just transfer the resolve/reject functions to wherever you need them using either closures or objects.
Actually initial discussion which "inspires" me on this thoughts was in
cancellation-tokens-related
topic. So here I didn't mean that it is impossible to implement that kind
of functionality on "home made" promises. I mean that it would be good to
have such functionality to "cancel"/"interrupt" execution flow of natively
generated promises, in particular to do some "cancellation" of async
task represented by async function or "unsubscribe" callback in
construction consumer = provider.then(treatSomehow); and then write
consumer.throw(new CancelationError("some reason")) instead of writing
consumer.cancel() which might be "controversial" (whether we intend to
cancel provider too, in which form we would like to do that (on which
"rail" that cancellation should be delivered? on success-rail, on
failure-rail or on some other extra-toxic cancellation-rail)). Ofcourse it
can be some very raw tools to do cancellation, but if they were present,
any "home made cancellation" would be much more doable and easy to
implement then it is now.
Regarding finally/return relationship I didn't mean that it is some
special feature of generators, instead, ofcourse I first of all check it on
regular functions (not only on js but in java too), and for me it was more
remarkable that return can be "replaced" with break or
continue by the means of finally block which is more interesting
case then replacing return with trow or vise versa (which ofcourse
also may lead to rather "strange" behavior and rare developer can guess
that and/or imagine that strange situation before someone else told him
about it). So putting that example with implicit return <-->
continue replacement I didn't mean to show some "extra" behavior of
generator.return() I only would like to highlight that function*
generators implementation of .return is QUIT VALID and really simulates
return behavior in place of yield instruction (without omitting any
strange "outcome" on execution flow which finally block may produce).
And it is actually one of key differences with how bluebirdjs cancleation works
as follows from bluebirdjs-cancellation. There is assumed that
cancellation action cause that "only active finally blocks (finally
callbacks) are executed" and then cancellation completes (there are no
chance to "override" cancellation propagation from finally callbacks in
that api) while in "real code" it is possible (and in generators to ...).
By the way in flavor of Promise.resolve and Promise.reject I would
rather "rename" Q.deferred into Promise.never, in fact it would be
almost the same - factory of some promises that never completes it self by
its own but rather wait until someone pushes it from outside by the means
of abnormal .return or .throw. It looks maybe little bit strange, but
describes the thing nicely :)
Assuming analogy in tuples (
function*,yield,Generator.prototype.return(),Generator.prototype.throw()) and (async function,await,?Promise.prototype.return(),?Promise.prototype.throw()), it would be good to havePromise.prototype.return()andPromise.prototype.throw()which in case ofasync/awaitfunction should just "inject" somereturn/throwaction in place of pendingawaitstatement.While for
.then-like promises it should just force promise to abnormal completion (with provided result) without calling callbacks provided in .then/.catchmethods (if they was not yet called before, at the time of "abruption" attempt).In case of general
new Promise(promiseExecutor)-like promises it supposedly should work in following manner: someabnormalReturn/abnormalThrowsignals should be sent topromiseExecutor, thenpromiseExecutorcan handle this signal and initialte "preventDefaultbehavior" which will correspond tocatch/finallyblocks capabilities, orpromiseExecutorcan ignore and not handle this signals, then default behavior should be invoked (which will lead to promise abnormal completion with value provided inPromise.prototype.return(...)/Promise.prototype.throw()call) - this second option will correspond to that case when assumptive meta-code ofpromiseExecutordoes not contain imaginarycatch/finallyblocks.In code that last case might look like this:
const waitAsync = (delay) => (new Promise( (setOk, setErr, signalsSource) => { const beforeComplete = () => {clearTimeout(tid);} const tid = setTimeout( () => {beforeComplete(); setOk();}, delay ); signalsSource.on( "abnormalReturn", (abnormalReturnSignal) => { if (abnormalReturnSignal.value < 5) { // reject offered for abnormal rerun value when it < 5 abnormalReturnSignal.preventDefault(); return; } // do some finally-like cleanup assuming that // abnormalReturnSignal default behavior will be launched beforeComplete(); ); signalsSource.on( "abnormalThrow", (abnormalThrowSignal) => { if (abnormalThrowSignal.value instanceof HatedError) { // catch and reject HatedError-es, corresponds to catching this one // and re-throwing/propagating anything else by abnormalThrowSignal default behavior abnormalThrowSignal.preventDefault(); return; } // do some finally-like cleanup assuming that // abnormalThrowSignal default behavior will be launched beforeComplete(); ); } )); // waitTask = waitAsync(100); // waitTask.then((value) => {console.log(`wait ok, value = ${value}`)}); // waitTask.return(3); - abnormal return should be rejected by promice executor logic, since 3 < 5 // waitTask.return(103); - abnormal return should happen since !(103 < 5) and default behavior should work // "wait ok, value = 100" message should be printed, without delay in 100msIn case if some transpiler decide to implement
async functions using generator functions thispromise.return(...)andpromise.throw(...)functions can be obtained almost out-of-the-box usingGenerator.prototype.return()andGenerator.prototype.throw().Approximately it might look like:
var someFunctionToTranslate = function (...args) { // ... some code here ... var someValue = await somePromice; // ... some code here ... };might be translated to
var someFunctionToTranslate = _rawGeneratorFuncToNormalAsyncLikeFunc(function* (...args) { // ... some translated code here ... var _tmpAwaitTask = new AwaitTask(somePromice); yield _tmpAwaitTask; var someValue = _tmpAwaitTask.awaitResult; // ... some translated code here ... });Then
promice.return(...)on obtained promise should causegenerator.return(...)on underling generator object, and lead to abnormal completion delivered throughyieldinstruction which here represents a part ofawaitstatement translation.By the way during my reading of
Generator#return()andGenerator#throw()after reading bluebirdjs-cancellation I've suddenly aware that nativereturnstatement can be "completely rejected" by the means offinallyblock, meaning that in general for example there are no 100%-guaranties thatGenerator#return()will force generator to stop iterating same as there are either no guaranties thatGenerator#throw()will force generator to stop (but this one is more obvious regardingcatchstatement natural behavior).Here is some patched example from
Generator#return()function* gen() { // shielded yield 1 do { try { yield 1; } finally{ continue; } } while (false); // shielded yield 2 do { try { yield 2; } finally{ continue; } } while (false); // shielded yield 3 do { try { yield 3; } finally{ continue; } } while (false); } var g = gen(); console.log(g.next()); // { value: 1, done: false } console.log(g.return("foo")); // { value: 2, done: false } // impudent generator rejects return console.log(g.return("bar")); // { value: 3, done: false } // impudent generator rejects return console.log(g.next()); // { value: undefined, done: true } // generator finishes by its own willSo considering this possibility, I would say, in case if
promise.return()would be implemented, provide possibility to promiseExecutor to intercept and override.return()action is quit natural (assuming that by default it will not handle that signals and notpreventDefaulton it, and that means - in general work like regular code withoutfinally/strange-finallyblocks)