Open slightlyoff opened 11 years ago
a "direct" version of
.then()
, e.g. one which has the semantics of.accept()
and not.resolve()
for values returned from callbacks
I think I know what you mean by this, but just to be sure, could you tell us what the fulfillment value ("acceptance value"?) of x
, y
, and z
are in the following?
var a = new Future({ accept } => accept(5));
var b = new Future({ accept } => accept(a));
var x = a.when(() => 5);
var y = a.when(() => a);
var z = a.when(() => b);
I tried reading this but had a hard time understanding in between the cautionary notes and preferences stuff. The idea is to not recursively join promises on then
and resolve
. when called via new methods?
I know we're in JavaScript-land but some pseudo type-signatures would really clear this up for me.
Hey @domenic:
x == 5
, y == 5
, and z == a
At least that's my read of the design that @tabatkins was gunning for. I'm open to arguments that would make y == a
and z == b
, but I've been told that nobody wants that = )
Ah. I don't think that's very conceptually coherent :-/. If you want a counterpart to accept
, it should be y ~ a
and z ~ b
. I think that this desire for conceptual coherence was the main idea behind @erights's "The Paradox of Partial Parametricity".
My goal here is to remove the strain from .then()
. I think there is a missing method. What it does, I'm somewhat less concerned about.
Waiting for @tabatkins and @dherman to weigh in.
Like Dominic, I think that if we were to have a separate .when()
function, then it would be most useful/consistent if that function gave
x == 5
y == a
z == b
This is the result you'd get if the implementation of .when()
called .accept(returnValue)
rather than .resolve(returnValue)
.
But I could be biased because I think that .resolve()
and .then()
should never recursively unwrap promises. I rather think the question is if we should allow them to become deeply wrapped or not.
If we're going to have both .then()
and .when()
I would strongly prefer to have clearer names. I.e. where it's more clear what the difference in behavior is between them.
@domenic
x
is 5, y
is 5, and z
is Future<5>
, or a
.
@sicking Oh goodness, no. That would make it a map, which is much less useful. What I've wanted this whole time is a monadic bind function, which does single-level unwrapping. (Once you have bind, you get map for free.)
@slightlyoff
If we're going to go this way, I think it would probably be worthwhile to be strict about things, and make .when()
strict about the return values of its callbacks. Right now it's loosey-goosey, acting like a bind if it can, and falling back to a map otherwise. Being strict means that x
would be a rejected promise containing a TypeError, because the return value of the callback wasn't a promise, then y
is 5 and z
is a
.
That behavior is more predictable, though less generally useful. I might be okay with this - .then()
is the really loose, friendly function that accepts direct values or promises, and it fully unwraps so that its returned promise only ever contains direct values. .when()
is the stricter and more predictable monadic function.
In that case, let's call the function .chain()
instead, like @puffnfresh does in his Fantasy Land spec. It's a good general-purpose name for the monadic binding operation (I never really understood why Haskell calls it "bind" in the first place), which means that other structures can re-use the same name without trouble, and we can do good monadic stuff. It's also further from .then()
in name-space, which should help people to distinguish them, and I think it's somewhat clearer in general - you're chaining two promises together, which is why you can't return a non-promise.
:+1:
https://github.com/puffnfresh/fantasy-land#chain
The chain
method would allow us to immediately make use of Fantasy Land derived functions for DOMFutures, which would be really great. The only requirement is that the law associativity is obeyed, which seems to be true.
So, some details you missed: ^_^
(Assuming that the new operation is called chain
.)
Your proposal details differentiate between then
and chain
by the way they treat their callback return values, with then
fully unwrapping and chain
single-unwrapping. This works fine if you only use then
or only use chain
, but if you mix them, you'll run into trouble - a then
callback can receive a Future, or a chain
callback can have its argument unceremoniously collapsed.
Instead, they should differentiate on the argument side - if the Future contains nested Futures, the then
callbacks don't get called until they all resolve and you end up with a plain value, while the chain
callbacks fire as soon as the outer future resolves. It's possible for a then
callback to return a nested Future, but if you then continue to use then
you won't ever see it.
Anyone willing to create (preferably real-world) JS examples of when & how you'd use one over the other? My first reaction is the single unwrap is weird but feel I'm missing something
Still hoping @dherman will weigh in, but a few observations:
.chain()
, particularly not with the strictness stipulations. It's clear that it's dealing in Futures and not values under those conditions, and the name points in that direction..chain()
right now and it can be added later. A stand-alone chain(f, cb)
method that takes a Future
and callback gets you there on the back of the current design. You don't need anything new from Futures to effect this, AFAICT. Am I wrong about that?Thanks.
@jakearchibald Lots of discussion about this in the various threads. Single-unwrap helps in scenarios where different futures represent fundamentally different types of things. For example, a database retrieval might return a Future, and the thing stored in the database might be a Future. You may want to just wait for the database retrieval to finish and then get to work on the value immediately, rather than having to wait for both of them to finish.
This is especially true when we add LazyFuture or whatever, which doesn't start its operation until someone registers a callback.
I don't like multi-unwrap at all (unless done explicitly), because I think generally it'll just paper over programming errors, but fuck it, if I can get a good chain
out of this, I'll accept a recursive-unwrapping then
.
@slightlyoff I don't understand. Your starting post in this thread was attempting to resolve the issue by splitting it into two operations, and now you're trying to suggest that we don't need two operations at all? Don't bait-and-switch me, bro.
But no, you can't build chain
on top of recursive-unwrapping then
without bullshit extra wrappers designed solely to foil auto-unwrapping, because your Futures won't stack. It would be extremely clumsy and dumb, and wouldn't work like a normal Future at all.
If only there were an alternative method like .then()
which exposed the new Future's resolver to it's callbacks.
Then you could accept / reject / resolve your intended .then()
callbacks in anyway you need.
So you could do:
.thenWithResolver( resolveOnce( getSomething ) ) // unwrap once if getSomething(value) returns a promise
.thenWithResolver( accept( getSomething ) ) // always accept, even if only a promise
.thenWithResolver( rejectPromises( getSomething ) ) // Don't give me promises!
.thenWithResolver( rejectPromiseStreams( getSomething ) ) // I'm not trusting your promise a second time!
.thenWithResolver( future_invoke( obj, "getSomething" ) ) // .then( obj.getSomething.bind(obj) )
.thenWithResolver( translatePromises( getSomething ) ) // It's a promise Jimmy, but not as we know it.
It's just an idea off the top of my head really.
In general though, the right way to do all these things is to create another new Future inside a .then()
callback.
@shogun70 What you're saying doesn't make any sense. Neither have any connection to the question of nested promises.
Returning a Future from a .then() callback is already a useful and accepted pattern - it gets unwrapped and the chained future adopts its state. The question is about returning a Future for a Future.
@slightlyoff So, the best, most consistent model is this:
Futures can stack. No magic, nothing special, you can just wrap anything in a Future, including more Futures. then
waits until all nested futures have resolved before calling its callbacks with the final non-future value. chain
just waits for the outermost future, and calls its callbacks with whatever the value of that is, whether it's a plain value or another future. Both are the "simpler" option, depending on who you ask.
(The treatment of callback return values isn't strictly important, but then
can accept either futures or plain values, treating the latter as if it had been wrapped in an accepted future. chain
only accepts futures. Both have their chained future adopt the returned future's value, doing single-level unwrapping. Of course, if you keep using then
, the fact that it only unwraps the return value one level is invisible, since your callback isn't called until all the levels are unwrapped.)
This solves all problems, forever. If you have a nested Future where the two Futures are substantially different kinds of things, such that you only want to wait for the outer future to finish, not both, you can use chain
to interact with it. If you don't care about all those details, and just want the final value, you can use then
. There are use-cases for both - I've given use-cases for chain
, and the use-cases for then
are obvious, and all of the "I don't give a crap about your asynchrony, just call me when you're done" variety.
This also happens to satisfy people who like monads, because chain
is the monad operation over futures.
It's possible to build then
on top of chain
without too much difficulty, but you can't do the opposite without a lot of annoying bullshit, like a non-thenable wrapper object designed solely to foil the recursive resolving.
So just to report on the TC39 meeting's results, we have made the following changes to a new Promise.idl
that is now the basis for discussion:
.then()
as we know it continues.fulfill()
is the new .accept()
and is now available along-side .resolve()
.chain()
operator, but it can be added by subclassesI accept that .chain()
explains .then()
and not vice-versa, but I'm not sure that the new current state is bad: it neither precludes adding .chain()
in the future, nor does it prevent libraries from adding their side-contract version, nor does it prevent anyone from advocating for their preferred style.
Ugh, that's incoherent, though! Setting aside chain
for the moment, the consensus you describe means that then
is not guaranteed to get a plain value - authors can construct a nested future with fulfill
and then call .then()
on it, and the callback gets a future, not a value.
This is just silly. If you want Mark/Domenic's then
, then either remove fulfill
so you can never produce a nested Future (I don't recommend this) or make then
do the unwrapping at the read side, not the return side. The difference is unobservable to code that only produces singly-wrapped Futures, but it acts much better when nested Futures show up.
And no, you can't introduce chain
via subclasses. Just try it. The only way to do it has already been demonstrated by Domenic, and it produces something that is unusable by things that expect normal promises without special-case ugliness. It also requires ES6 argument destructuring just to be usable; without that, it's beyond the pale in usability. Even with destructuring, it's not all that usable - Domenic uses an {x}
in his code, which looks like a standard placeholder argument name, but it's not - x
is actually part of the contract, and you have to use it (or use the longer {x:foo}
form to name your argument foo
).
It's just functional composition @tabatkins.
.then()
callbacks don't need to be modified - they can still return a value-or-promise. Or a promise-for-a-promise.
To modify the way you handle those things - single unwrap; infinite unwrap; immediate accept; etc - it's just the wrappers that need to change.
So,
.then( getSomething )
can receive exactly the same getSomething
callback as
.thenWithResolver( resolve ( getSomething ) )
Assuming thenWithResolver
passes its new future's resolver as the first arg (like new Future()
init callbacks),
resolve
is simply:
function resolve( fn ) {
return function( r, value ) { // this doesn't need try-catch, because ...
result = fn( value ); // ... if this throws `.thenWithResolver` will reject
r.resolve( result );
}
}
In long hand that would be:
function resolve( fn ) {
return function _resolve( r, value ) {
result = fn( value );
if ( isThenable ( result ) ) { // approximately: ( result && typeof result.then === 'function' )
result.then ( _resolve.bind( null, r ), r.reject );
}
else r.accept( result );
}
}
Continuing on:
.thenWithResolver( accept( getSomething ) ) // always accept, even if only a promise
...
function accept( fn ) {
return function( r, value ) { r.accept( fn (value ) ); }
}
chain
seems to be a single unwrap:
.thenWithResolver( resolveOnce ( getSomething ) ) // unwrap once if getSomething(value) returns a promise
...
function resolveOnce( fn ) {
return function ( r, value ) {
var result = fn( value );
if ( isThenable ( result ) {
// unwrap once and then accept, even if a promise
result.then( r.accept, r.reject ); // assuming `accept` and `reject` are bound methods
}
else r.accept( value );
}
}
This approach seems quite straight-forward. I can't see any difficulties for someone who knows enough to need non-default behavior.
@shogun70 I've argued this with you in the mailing list, and won't repeat it here. Your proposal is less convenient for common cases, in return for being slightly more convenient for interfacing with legacy callback APIs. It doesn't match any of the major promise libraries.
You are reacting to a non-essential implication of my proposal which I haven't even mentioned in this discussion.
Lower-level alternatives for then
/ catch
don't require the deprecation of then
/ catch
.
And they provide more flexibility, including the feature you are after.
Why is it up to the API to say that JS devs will only want the default behavior?
You are very aware that some JS devs want non-default behavior, because you are one of those people. Why limit that non-default behavior to only one option, when you can leave that decision to the coder?
Every time I read these javascript discussions on Future
, I see an emphasis on the fact that it is a monad. The Future
monad brings great benefits. Of course, we'd also need a library that parameterises on all monads to truly exploit this benefit. Good luck with that! However, this is not why I write.
Rather, I wish to stand in place and defend the utility of the forgotten future function. Future
is a semi-comonad and I further this claim with the following: while future's monad is ubiquitous and useful in everyday programming, its dual, the semi-comonad, is at least just as much, if not more!
The following table demonstrates the relevant functoriality to this discussion.
Future
is a covariant functor i.e. has map
obeying laws of identity and composition.
(a -> b) -> Future a -> Future b
Future
is an applicative functor i.e. has ap
obeying laws of identity, composition, homomorphism and interchange.
Future (a -> b) -> Future a -> Future b
Future
is a monad i.e. has bind
obeying law of associativity and point
being the identity for bind
.
(a -> Future b) -> Future a -> Future b
Future
is a (semi-)comonad i.e. has cobind
obeying law of associativity.
(Future a -> b) -> Future a -> Future b
Please do not forget my friend, cobind
.
Indeed, Future has cobind
as well (or as I usually see it named, extend
). You can implement that on top of either Future design. ^_^
The problem with this API is that it is neither intuitive, nor symmetric for the lay man.
accept()
and reject()
are clear, so are success()
and failure()
. Once you begin talking about resolve()
or fulfill()
you went off in the wrong direction.
I suggest renaming:
fulfill()
to success()
resolve()
to next()
or chain()
reject()
to failure()
then()
to onSuccess()
catch()
to onFailure()
These might not be the most elegant names, but they are damn intuitive (at least to me).
Please rename the API methods to something more intuitive and symmetric. Thank you!
Doing this here because the mailing lists have turned into an unfollowable mess.
First, some cautionary notes: this thread will use the terms "wrapping" and "unwrapping" to refer to APIs that treat
Future
s as something other than direct values. Posts here that use different terminology will be edited to conform.Next, please be civil and cordial. Those who don't agree with you aren't bad and they might not even be wrong. Goodwill will get us where we want to go.
OK, down to it:
It seems to me that the contended bit of the design is
.then()
(and by extension,.catch()
). Both pro and anti-unwrapping camps can have their own way at resolution time using.resolve()
and.accept()
respectively. And both have valid points:On top of these points, there are preferences for styles of use. These can't ever be truly enforced with rigor because it's trivial to write wrapping/unwrapping versions of direct functions and vice versa. So there are cultural differences that we must admit with regards to preferred styles of use.
With the caveat that all involved are willing to live-and-let-live, I observe that it is possible to end the current debate in everyone's favor so long as we add a "direct" version of
.then()
. That is to say, one which has the semantics of.accept()
with one level of unwrapping for a returned thenable and not.resolve()
(infinite unwrapping) for values returned from callbacks.I'll submit
.when()
as a straw-man name, but I'm not precious about it.So the question now is: is everyone cc'd here willing to make such live-and-let-live compromise? And if not, why not? And is there some other detail which I've missed?
/cc @domenic @dherman @wycats @annevk @tabatkins @erights