The argument for avoiding promises for promises is fairly simple. A promise is a representation of a value that will exit in the future. If that value is still a promise, you haven't really got to the final value yet. Conceptually having the promise doesn't really provide anything other than a means to obtain the final value, as such it's of no additional use to be able to get the intermediate nested promises: You might as well just jump straight to the value.
This problem is explained in more detail by @erights here. The currently proposed [[Resolve]]
algorithm only dictates this recursive flattening for unrecognised thenables
, not for promises. THe advantage of this behavior is that a user can pick up a Promises/A+ library and return a deeply nested mess of foreign promises objects (all of which were broken enough not to do any unwrapping), and in a single step get back to the actual value being represented.
In the following example, you would ideally expect Q
to be equivallent to Q.then(identity)
var p = new Promise(resolve => resolve(5));
var q = new Promise(resolve => resolve.fulfill(pFor5));
var P = p.then(x => x); // P is a promise for 5, just like p
var Q = q.then(x => x); // oops! Q is a promise for 5, *unlike* q.
Promises are meant to make it easier to make synchronous code into asynchronous code. With this in mind we have to ask ourselves what the synchronous analog of any new feature would be. This is easiest to see in the case of a promise-for-a-rejected-promise.
A rejected promise from a function call represents that the function threw (asynchronously). What does a return value of a promise-for-a-rejected-promise represent? It seems to represent having called another internal function, that itself threw. This has no synchronous analog.
Similarly, of course, with return values: if f(x)
returns g(x + 1)
, and g(y)
returns y * y
, then f(x)
returns (x + 1) * (x + 1)
; it does not return some wrapped representation of g(x + 1)
that must then be unwrapped to get (x + 1) * (x + 1)
.
Another way of viewing the synchronous analog issue is that chaining a new .then
call is like adding another frame to the stack, this is easier to reason about if the promises are always fully flattened, rather than having a separate flatten operation. This argument does not really cause a problem for having then
callbacks that only unwrap one layer, it just makes clear that they must unwrap at least one layer.
The Parametricity argument in favour of promises for promises is essentially completely circular. It can be boiled down to:
- Promises for Promises should be allowed for everything because we should allow Promises for anything and not restrict the values a Promise can represent.
This isn't really an argument since it depends on it's conclusion to be true.
Right now, I assume that - in principle - every promise could aside from its
then
method, have aflatten
method too, which would resolve the variant that is returned now. Also, for convenience, it could have areallyThen
method which would come down to callingflatten().then
.I think the idea of a
reallyThen
looks nasty.Calling
flatten
inside chains (.flatten().then
instead of just.then
) of promises increases noise . On the other hand, depending on your knowledge about promises returned by your code, you may be sure you don't have to callflatten
. You know you'll be dealing with flat promises, where flatten would come down to a noop. (At least, I think such a setup would be possible. I'd have to think about implementation.)One potential (but admittedly a bit far-fetched) way out, and directly addressing the job of translating "synchronous"/serial execution of methods into async code would be to use promised or lazy builtins (see https://github.com/braveg1rl/promised-builtins for a sketch). This would entirely abstract away the fact whether a promise is a "flat promise" or a "nested promise" because values only have to resolve when actually needed (say when calling
forEach
on a array) and the library code can do the "noisy" job of callingflatten
for you. You also don't have the noise of then or promise chains. You just see sync-like code.Otherwise, it can be argued that instead of changing behavior of
then
, we could benefit from two new methods:flatten
andlater
.later
would do entirely the same asthen
, except that it won't flatten. Internally, the return value for.then(onResolved, onRejected)
can then be computed asthis.flatten().later(onResolved, onRejected)
. (at least, that seems nice code-reuse to me).Does this make sense?