Skip to content

Instantly share code, notes, and snippets.

@ForbesLindesay
Last active April 18, 2017 01:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ForbesLindesay/5392612 to your computer and use it in GitHub Desktop.
Save ForbesLindesay/5392612 to your computer and use it in GitHub Desktop.

Arguments Against Promises for Promises

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.

The "JQPFAQPFAWJPFADFFAN" Problem

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.

The identity function breaks

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.

Synchronous Analog

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).

Stack frame

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.

Parametricity

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.

@myrne
Copy link

myrne commented Apr 16, 2013

translation of synchronous code to asynchronous code is easier with transitive .then() semantics, rather than an external higher order .flatten()

Right now, I assume that - in principle - every promise could aside from its then method, have a flatten method too, which would resolve the variant that is returned now. Also, for convenience, it could have a reallyThen method which would come down to calling flatten().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 call flatten. 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 calling flatten 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 and later. later would do entirely the same as then, except that it won't flatten. Internally, the return value for .then(onResolved, onRejected) can then be computed as this.flatten().later(onResolved, onRejected). (at least, that seems nice code-reuse to me).

Does this make sense?

@myrne
Copy link

myrne commented Apr 16, 2013

All in all, I would like to have both.
That is, I'm in favor of keeping current behavior of then for its simplicity in creating typical promise-chains.
And I'm also much in favor of the idea that libraries support a way of not "eagerly" resolving promises. It can just be put in another method.

@myrne
Copy link

myrne commented Apr 16, 2013

Given multiple promise systems that each recognize only their own promises as promises, but which mostly agree on the meaning of "then", assimilation turns out to be a surprisingly pleasant way for these to co-exist.

@domenic . How do promise libraries recognize their own promises as their own? Using instanceof or so?

[[Resolve]] only deals with assimilating foreign non-promise thenables

And do I infer correctly from the two quotes above that a promise library won't eagerly call then on its "own" promises?
If that's the case, and the flattening is a side-effect from attempting to normalize the promise behavior into something that's A+ compliant, then another option would be to make a new spec for recognizing A+ promises, even if they're not created by the library which is doing the recognition. There could be a property isPromiseAPlusCompliant on the prototype or constructor function of such promises. This would require some level of trust between library authors though. The library can't guarantee behavior of (all) its returned promises anymore, and has to trust in the isPromiseAPlusCompliant label of the promises it passes on unchanged.

the spec does specify that if a non-promise thenable is assimilated, i.e. an unrecognized thenable from another implementation, it must be flattened. This behavior is meant to preserve boundaries between implementations.

This lends further assurance to my interpretation of what you said. I'll dig into RSVP to get some clue. :)

In RSVP I can't really see its own promises being special-cased. At least not with instanceof. So maybe NOT eagerly calling then on a libraries own promise is not specified in the spec, bu it is isn't ruled out either? Left up to the implementor?
Is there any library which currently special-cases its own promises?

@myrne
Copy link

myrne commented Apr 16, 2013

If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise.

Hypothetically change into

If x is an unrecognized thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise.

?

Then define "unrecognized thenable" further at some point.

I know that would change the behavior current libraries, but it appears to me like as if current behavior is not really on purpose (i.e. for the purpose of having nice behavior for any promise consumers), but is just done out of convenience to ensure assimilation in into a library's own promises. I.e. just assimilate any thenable, and you're sure to assimilate any foreign and/or misbehaving ones.

Having a library special-case it's own promises could be an improvement to the status quo then. As long as you remember to solely use one library, you can rely on non-flattening. But compatibility between A+ libraries would be nicer, which could be done with a "compliance label".

@erights
Copy link

erights commented Apr 16, 2013

@meryn writes "Is there any library which currently special-cases its own promises?"

See http://code.google.com/p/es-lab/source/browse/trunk/src/ses/makeQ.js , which is my own implementation of Q focussed on getting the security properties right. Kris Kowal and I intend to reconcile this and his Q.

"How do promise libraries recognize their own promises as their own? Using instanceof or so?"

Using a WeakMap, in which all Q-created promises are registered, where the power to register in that WeakMap is encapsulated within the Q library.

"This would require some level of trust between library authors though. The library can't guarantee behavior of (all) its returned promises anymore, and has to trust"

Exactly. That disqualifies such solutions as far as secure implementations of Promises/A+ are concerned.

@erights
Copy link

erights commented Apr 16, 2013

@meryn See http://research.google.com/pubs/pub40673.html for some examples where these security properties matter.

@erights
Copy link

erights commented Apr 16, 2013

@meryn writes "If x is an unrecognized thenable, ..." and "As long as you remember to solely use one library, you can rely on non-flattening."

I like that.

I'll also note that no library can realistically enforce flattening of thenables:

var willBeThenable = {};
var shouldntBeThenable;
var p2 = Q(p1).then(_ => willBeThenable).then(x => { shouldntBeThenable = x; });
// shouldntBeThenable shouldn't be thenable, since it was obtained by the success callback of
// a .then on a trustworthy promise
shouldntBeThenable === willBeThenable // true
willBeThenable.then = function(sk,fk) { return "gotcha"; };
// shouldntBeThenable is thenable

@domenic
Copy link

domenic commented Apr 16, 2013

@meryn it may help to think of this from the perspective of libraries that do not want to allow promises-for-thenables. Those libraries will do two things:

  • Never allow the creation of promises for thenables
  • Conform to the Promises/A+ spec by flattening thenables-for-thenables before creating promises out of them.

That is, they must be protected from foreign thenables-for-thenables, and cannot grant exceptions to libraries based on a isPromiseAPlusCompliant brand, which if present would allow promises-for-thenables to sneak into a system specifically designed to keep them out.

As you have noticed, RSVP follows this philosophy, by not special-casing any thenables but instead always recursively flattening any incoming thenables (and by not allowing the creation of promises for thenables).

@DavidBruant
Copy link

Some practical experience and examples on why the flattening property is convenient https://mail.mozilla.org/pipermail/es-discuss/2013-April/030192.html

@erights
Copy link

erights commented Apr 25, 2013

@DavidBruant's post inspired https://mail.mozilla.org/pipermail/es-discuss/2013-April/030198.html which should help illustrate the point.

@masaeedu
Copy link

@erights Fairly old post, but re:

Those who come from either a statically typed or monadic perspective, or have had no experience with flattening promises, generally
think they shouldn't flatten

When you say "flatten", do you specifically mean recursively flatten? My understanding is that the FP camp suggests then should flatten, and in fact should only flatten, and not support transformations that do not return a promise which it will immediately flatten. The primitive operation for a monad is flatMap, not map.

I haven't written any promise libraries, so I don't know if flattening promises recursively is a super common problem that keeps popping up for implementers all the time. In all my time using promises, I don't think I've never constructed any nested promises that were not immediately flattened as a consequence of being returned from a transformation passed to then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment