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
/await
function should just "inject" somereturn
/throw
action in place of pendingawait
statement.While for
.then
-like promises it should just force promise to abnormal completion (with provided result) without calling callbacks provided in .then
/.catch
methods (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
/abnormalThrow
signals should be sent topromiseExecutor
, thenpromiseExecutor
can handle this signal and initialte "preventDefault
behavior" which will correspond tocatch
/finally
blocks capabilities, orpromiseExecutor
can 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 ofpromiseExecutor
does not contain imaginarycatch
/finally
blocks.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 100ms
In case if some transpiler decide to implement
async function
s 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 throughyield
instruction which here represents a part ofawait
statement translation.By the way during my reading of
Generator#return()
andGenerator#throw()
after reading bluebirdjs-cancellation I've suddenly aware that nativereturn
statement can be "completely rejected" by the means offinally
block, 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 regardingcatch
statement 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 will
So 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 notpreventDefault
on it, and that means - in general work like regular code withoutfinally
/strange-finally
blocks)