Promises Consensus

# Tab Atkins Jr. (7 years ago)

Heya! I, Mark, and others have been hashing out our remaining differences on Promises privately, and are all happy with each other now, with only two remaining issues to be decided in a larger audience. Anne says that we should be able to get DOM Promises on track with this consensus if we finish up the discussion in the next month or so, since the differences from the current spec are mostly internal/new API.

Here's our current consensus:

Promises have both a .then() and a .flatMap() method.

  1. p.flatMap() does "single-level" resolution:

    • whatever p resolves to, gets passed to the flatMap() callbacks.
    • The callback return value must be a promise-like, which is adopted by the output promise; otherwise, the output promise rejects with a TypeError.
  2. p.then() does "recursive" resolution on the input side (per consensus following 2 TC39-meetings ago):

    • if p accepts to a promise-like, the callbacks get moved down to that until it either accepts with a non-promise-like, or rejects.
    • Rejection calls the rejection callback without delay; no extra resolution mechanics happen here.
    • The callback return value can be a promise-like or not. If it is, the output promise adopts it; if not, the output promise accepts it.
  3. The helper functions (Promise.every(), etc.) use .then() semantics. That is, Promise.every() will eventually resolve to an array of non-promise-likes.

The first issue still up for community discussion involves the definition of "promise-like".

We'd like the definition to be: (a) a Promise or subtype, or (b) a branded non-Promise (with the branding done via Symbol or similar). Promises/A+ wants the branding to be done via a method named "then".

This, unfortunately, goes directly against TC39 practices in a number of other areas, such as iterators, where we don't want short string names as branding due to the possibility of collision. (In the case of "then", collision isn't a possibility, it's a certainty - we know there are libraries out there today that put a "then" method on their objects without referring to Promises.) Thoughts?

The second issue still up for community discussion is what "adopts" means, precisely.

  1. Assume a .then() callback returns a non-native promise-like. We can't just use magic internal operations to detect when the returned value resolves, so the output promise will have to register callbacks on it. This appears to break our desire to have "lazy" promises in the future that don't compute a value until someone asks for it. Should we specify that adoption is done late? (That is, the output promise would hold onto the returned promise without touching it, until someone actually registers some callbacks on it.) This may have performance implications - is it possible that we just do eager resolution now, but later have detection for lazy promises getting returned and switch to lazy behavior in just those cases?

  2. Assume a .flatMap() callback returns a non-native promise-like. Obviously, the output promise adopts it by registering .flatMap() callbacks on it. But what if the promise-like only has a .then() method? Should we reject with a TypeError, or fall back to using .then() resolution semantics? (I suspect we need to do the former to maintain monad laws.)

# Domenic Denicola (7 years ago)

Just some terminology questions for this new proposal...

From: Tab Atkins Jr. [jackalmage at gmail.com]

whatever p resolves to, gets passed to the flatMap() callbacks.

What does "resolves" mean in this context? I don't believe you are using it in the same way that it is used in Promises/A+ or DOM Promises. For example, using the existing definition, you could resolve p to a forever-pending promise; I doubt you'd want to pass that forever pending promise to either (both?) of the flatMap callbacks.

if p accepts to a promise-like, the callbacks get moved down to that until it either accepts with a non-promise-like, or rejects.

What does "accepts" mean?

Promise.every() will eventually resolve to an array of non-promise-likes.

Again, what are you meaning by "resolve" here? Promises don't "resolve" to anything, so this is confusing.

We can't just use magic internal operations to detect when the returned value resolves, so the output promise will have to register callbacks on it.

Same question.

This may have performance implications - is it possible that we just do eager resolution now, but later have detection for lazy promises getting returned and switch to lazy behavior in just those cases?

Here I believe you are using "resolution" in the same sense as Promises/A+ or DOM Promises, but differently from all the above uses.

Should we reject with a TypeError, or fall back to using .then() resolution semantics?

Again this seems correct, but in contradiction to earlier uses.

# Tab Atkins Jr. (7 years ago)

[Gah, resending because I'm being way too loose with my terminology. Ignore previous email - this one has identical content, but uses terms correctly.] (Scratch that, I added a new point #3 at the end of the email.)

[For the purposes of this email, a promise "accepting" or "rejecting" means that its resolver's accept() or reject() method was called, or the equivalent internal magic. "fulfill" means "accept or reject". "resolve" means "adopt or accept, depending on whether the value is a promise-like or not" (in other words, what the resolver's resolve() method does). "adopt" means accepting or rejecting with the same value as the adopted promise. If I should be using better terms, let me know.]

Heya! I, Mark, and others have been hashing out our remaining differences on Promises privately, and are all happy with each other now, with only two remaining issues to be decided in a larger audience. Anne says that we should be able to get DOM Promises on track with this consensus if we finish up the discussion in the next month or so, since the differences from the current spec are mostly internal/new API.

Here's our current consensus:

Promises have both a .then() and a .flatMap() method.

  1. p.flatMap() does "single-level" resolution:

    • whatever p fulfills to, gets passed to the flatMap() callbacks.
    • The callback return value must be a promise-like, which is adopted by the output promise; otherwise, the output promise rejects with a TypeError.
  2. p.then() does "recursive" resolution on the input side (per consensus following 2 TC39-meetings ago):

    • if p accepts to a promise-like, the .then() callbacks get moved down to that promise-like until it either accepts with a non-promise-like, or rejects.
    • Rejection calls the rejection callback without delay; no extra resolution mechanics happen here.
    • The callback return value can be a promise-like or not. If it is, the output promise adopts it; if not, the output promise accepts it.
  3. The helper functions (Promise.every(), etc.) use .then() semantics. That is, Promise.every() will eventually accept to an array of non-promise-likes.

The first issue still up for community discussion involves the definition of "promise-like".

We'd like the definition to be: (a) a Promise or subtype, or (b) a branded non-Promise (with the branding done via Symbol or similar). Promises/A+ wants the branding to be done via a method named "then" (the "thenable" concept).

This, unfortunately, goes directly against TC39 practices in a number of other areas, such as iterators, where we don't want short string names as branding due to the possibility of collision. (In the case of "then", collision isn't a possibility, it's a certainty - we know there are libraries out there today that put a "then" method on their objects without referring to Promises.) Thoughts?

The second issue still up for community discussion is what "adopts" means, precisely.

  1. Assume a .then() callback returns a non-native promise-like. We can't just use magic internal operations to detect when the returned promise fulfills, so the output promise will have to register callbacks on it. This appears to break our desire to have "lazy" promises in the future that don't compute a value until someone asks for it. Should we specify that adoption is done late? (That is, the output promise would hold onto the returned promise without touching it, until someone actually registers some callbacks on it.) This may have performance implications - is it possible that we just do eager resolution now, but later have detection for lazy promises getting returned and switch to lazy behavior in just those cases?

  2. Assume a .flatMap() callback returns a non-native promise-like. Obviously, the output promise adopts it by registering .flatMap() callbacks on it. But what if the promise-like only has a .then() method? Should we reject with a TypeError, or fall back to using .then() resolution semantics? (I suspect we need to do the former to maintain monad laws.)

  3. For that matter, what about adopting the returned promise value of a .then() callback? If you try and use .then() to listen for the returned promise to fulfill, you'll end up imposing full recursive semantics on the output promise, regardless of whether they're observed with .flatMap() or .then(). Looks like we should default to trying to adopt with .flatMap(), and then maybe fall back to .then() for .then()-returned promise-likes.

# Domenic Denicola (7 years ago)

From: Tab Atkins Jr. [jackalmage at gmail.com]

For the purposes of this email, a promise "accepting" or "rejecting" means that its resolver's accept() or reject() method was called, or the equivalent internal magic. "fulfill" means "accept or reject". "resolve" means "adopt or accept, depending on whether the value is a promise-like or not" (in other words, what the resolver's resolve() method does). "adopt" means accepting or rejecting with the same value as the adopted promise. If I should be using better terms, let me know.

Thanks for the clarifications :). I think this is a bit confusing because it is at odds with commonly-used terminology, from DOM Promises and Promises/A+, but at least now things are defined and used in a self-consistent way. Much appreciated.

For the record, since you asked for better terms, the community consensus is:

  • "Fulfill" and "reject" are the two end states (as opposed to "pending").
  • "Settle" means "fulfill or reject."
  • "Resolve" means "adopt or fulfill."

"Accept" was just a strange neologism introduced by the linguistic fork that was DOM Futures, now thankfully dead.

But for the purposes of this thread it may be best to stop worrying about these issues now that you've set out a set of self-consistent terminology, and simply go with the terms as you defined them. We can always re-kill the zombie "accept" at a later date, replacing it with the normal "fulfill," once we understand its semantics.

I'll step back and let everyone else comment now, as I believe my views on the proposed semantics are well-known.

# Tab Atkins Jr. (7 years ago)
  • "Settle" means "fulfill or reject."

Ah, I'd never heard the term "settle" before, in any of the threads across the WGs here. Got it.

# Juan Ignacio Dopazo (7 years ago)

Does this all mean that you're ok with having promises-for-promises?

# Mark S. Miller (7 years ago)

For some meaning of ok, yes. It is clear that the DOM folks are proceeding full speed with promises, but are willing to be compat with a tc39 consensus if a tc39 consensus can be formed quickly enough. It was clear from the May tc39 meeting that promises that did not support promises-for-promises could not achieve consensus fast enough to serve this purpose. Due to Tab's very clever AP2 proposal, those who want to live in a .then world without promises-for-promises can (for most purposes) effectively do so. While the existence of .flatMap/.accept satisfies those who insist that a more purely monadic view, supporting promises-for-promises be exposed.

Nothing that has happened since then changes my opinion of the technical merits of the case. Five years from now we will look back and wish these two styles had simply been two distinct abstractions that had nothing to do with each other. But with the AP2 design, the costs of supporting both styles in one API are minimized. Tab did a great job finding a livable compromise. We are on track for agreeing on something in time to avoid a design fork by DOM promises.

# Tab Atkins Jr. (7 years ago)

On Wed, Jul 31, 2013 at 12:48 PM, Juan Ignacio Dopazo <dopazo.juan at gmail.com> wrote:

Does this all mean that you're ok with having promises-for-promises?

I've always been okay with that. ^_^ This consensus details how to handle nested promises (use .flatMap()) and how to ignore that and just get values out of them (use .then()). Everyone's happy!

# Mark S. Miller (7 years ago)

On Wed, Jul 31, 2013 at 11:23 AM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

[Gah, resending because I'm being way too loose with my terminology. Ignore previous email - this one has identical content, but uses terms correctly.] (Scratch that, I added a new point #3 at the end of the email.)

[For the purposes of this email, a promise "accepting" or "rejecting" means that its resolver's accept() or reject() method was called, or the equivalent internal magic. "fulfill" means "accept or reject". "resolve" means "adopt or accept, depending on whether the value is a promise-like or not" (in other words, what the resolver's resolve() method does). "adopt" means accepting or rejecting with the same value as the adopted promise. If I should be using better terms, let me know.]

Hi Tab, thanks for doing this. It is wonderful to have this in front of the community and to restart the public discussion.

You say "If I should be using better terms, let me know." As is, because of the terminology changes I find your message hard to read. (Worst for me was ' "fulfill" means "accept or reject" '.) Domenic's message is much closer[1] to the terminology as I think of it, as the Promises/A+ community has been using it, and as I think I've been using it in our private discussions. I'm wondering if you could simply repost this yet again with, approximately, Domenic's terminology? Of course, anytime Domenic's terminology doesn't fit the distinctions you're trying to convey, do what you need to do but note the difference. Thanks. This will make the summary much easier for many of us to follow.

[1] One thing I think Domenic is missing that I also missed at first: Once we introduce .flatMap, then we need a distinct "accepted" state that is neither "fulfilled" nor "rejected". The issue is that p.then does not fire until the promise p is fulfilled or rejected. If q is pending, and p is accepted to q, then p.flatMap will fire but p.then will not yet fire. When q becomes fulfilled or rejected, then p becomes fulfilled or rejected and p.then fires. Thus, p is following q. So when p and q are both promises, p follows q when p is accepted to q or when p adopts q. This hair splitting goes beyond any previous conversations I've had with anyone, but becomes necessary to account for the behavior or both .flatMap and .then under AP2.

# Domenic Denicola (7 years ago)

From: Mark S. Miller [erights at google.com]

One thing I think Domenic is missing that I also missed at first: Once we introduce .flatMap, then we need a distinct "accepted" state that is neither "fulfilled" nor "rejected". The issue is that p.then does not fire until the promise p is fulfilled or rejected. If q is pending, and p is accepted to q, then p.flatMap will fire but p.then will not yet fire. When q becomes fulfilled or rejected, then p becomes fulfilled or rejected and p.then fires. Thus, p is following q. So when p and q are both promises, p follows q when p is accepted to q or when p adopts q. This hair splitting goes beyond any previous conversations I've had with anyone, but becomes necessary to account for the behavior or both .flatMap and .then under AP2.

Isn't this just what we've been calling "resolved"? As in "p is resolved q, but still pending because q is pending"?

I suppose that is ambiguous because you could resolve p to a non-promise-like and the behavior is a bit different. Perhaps you're proposing that "resolve p with q" will make p resolved with q, and we will additionally say either that p is accepted with q, if q is a promise-like, or fulfilled with q, if q is non-promise-like. Does that sound accurate?

# Claude Pache (7 years ago)

Le 31 juil. 2013 à 20:23, "Tab Atkins Jr." <jackalmage at gmail.com> a écrit :

The first issue still up for community discussion involves the definition of "promise-like".

We'd like the definition to be: (a) a Promise or subtype, or (b) a branded non-Promise (with the branding done via Symbol or similar). Promises/A+ wants the branding to be done via a method named "then" (the "thenable" concept).

This, unfortunately, goes directly against TC39 practices in a number of other areas, such as iterators, where we don't want short string names as branding due to the possibility of collision. (In the case of "then", collision isn't a possibility, it's a certainty - we know there are libraries out there today that put a "then" method on their objects without referring to Promises.) Thoughts?

I suggest an @@isPromise builtin symbol, which works the same way as @@isRegExp in the ES6 spec [1]: An object is reputed to be a promise if and only if it has a property (either own or inherited) named @@isPromise. And Promise.prototype has initially an @@isPromise own property, so that instances of subclasses of Promise are recognised as promises.

(With this solution, you have not to choose between subclassing or branding, but you have the both. :-) )

—Claude

[1] search the occurrences of @@isRegExp in: people.mozilla.org/~jorendorff/es6-draft.html

# Mark Miller (7 years ago)

On Wed, Jul 31, 2013 at 3:52 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

From: Mark S. Miller [erights at google.com]

One thing I think Domenic is missing that I also missed at first: Once we introduce .flatMap, then we need a distinct "accepted" state that is neither "fulfilled" nor "rejected". The issue is that p.then does not fire until the promise p is fulfilled or rejected. If q is pending, and p is accepted to q, then p.flatMap will fire but p.then will not yet fire. When q becomes fulfilled or rejected, then p becomes fulfilled or rejected and p.then fires. Thus, p is following q. So when p and q are both promises, p follows q when p is accepted to q or when p adopts q. This hair splitting goes beyond any previous conversations I've had with anyone, but becomes necessary to account for the behavior or both .flatMap and .then under AP2.

Isn't this just what we've been calling "resolved"? As in "p is resolved q, but still pending because q is pending"?

I'm sorry Domenic, but since I'm hair splitting and stated several distinctions, I need to know which "this" you refer to.

# Tab Atkins Jr. (7 years ago)

On Wed, Jul 31, 2013 at 3:53 PM, Claude Pache <claude.pache at gmail.com> wrote:

Le 31 juil. 2013 à 20:23, "Tab Atkins Jr." <jackalmage at gmail.com> a écrit :

The first issue still up for community discussion involves the definition of "promise-like".

We'd like the definition to be: (a) a Promise or subtype, or (b) a branded non-Promise (with the branding done via Symbol or similar). Promises/A+ wants the branding to be done via a method named "then" (the "thenable" concept).

I suggest an @@isPromise builtin symbol, which works the same way as @@isRegExp in the ES6 spec [1]: An object is reputed to be a promise if and only if it has a property (either own or inherited) named @@isPromise. And Promise.prototype has initially an @@isPromise own property, so that instances of subclasses of Promise are recognised as promises.

(With this solution, you have not to choose between subclassing or branding, but you have the both. :-) )

The "or" up there wasn't meant to be an exclusive or. We intend either of them to be possible. Yes, symbols on the prototype gets you there with a single mechanism.

# Domenic Denicola (7 years ago)

From: Mark Miller [erights at gmail.com]

On Wed, Jul 31, 2013 at 3:52 PM, Domenic Denicola <domenic at domenicdenicola.com> wrote:

From: Mark S. Miller [erights at google.com]

One thing I think Domenic is missing that I also missed at first: Once we introduce .flatMap, then we need a distinct "accepted" state that is neither "fulfilled" nor "rejected". The issue is that p.then does not fire until the promise p is fulfilled or rejected. If q is pending, and p is accepted to q, then p.flatMap will fire but p.then will not yet fire. When q becomes fulfilled or rejected, then p becomes fulfilled or rejected and p.then fires. Thus, p is following q. So when p and q are both promises, p follows q when p is accepted to q or when p adopts q. This hair splitting goes beyond any previous conversations I've had with anyone, but becomes necessary to account for the behavior or both .flatMap and .then under AP2.

Isn't this just what we've been calling "resolved"? As in "p is resolved q, but still pending because q is pending"?

I'm sorry Domenic, but since I'm hair splitting and stated several distinctions, I need to know which "this" you refer to.

By "this" I meant the "accepted" state, and the idea that "p is accepted to q."

While I'm here, let's correct a typo: "As in 'p is resolved q..." becomes "As in 'p is resolved with q..."

# Mark S. Miller (7 years ago)

On Wed, Jul 31, 2013 at 4:02 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

From: Mark Miller [erights at gmail.com]

On Wed, Jul 31, 2013 at 3:52 PM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

From: Mark S. Miller [erights at google.com]

One thing I think Domenic is missing that I also missed at first: Once we introduce .flatMap, then we need a distinct "accepted" state that is neither "fulfilled" nor "rejected". The issue is that p.then does not fire until the promise p is fulfilled or rejected. If q is pending, and p is accepted to q, then p.flatMap will fire but p.then will not yet fire. When q becomes fulfilled or rejected, then p becomes fulfilled or rejected and p.then fires. Thus, p is following q. So when p and q are both promises, p follows q when p is accepted to q or when p adopts q. This hair splitting goes beyond any previous conversations I've had with anyone, but becomes necessary to account for the behavior or both .flatMap and .then under AP2.

Isn't this just what we've been calling "resolved"? As in "p is resolved q, but still pending because q is pending"?

I'm sorry Domenic, but since I'm hair splitting and stated several distinctions, I need to know which "this" you refer to.

By "this" I meant the "accepted" state, and the idea that "p is accepted to q."

While I'm here, let's correct a typo: "As in 'p is resolved q..." becomes "As in 'p is resolved with q..."

No. Assuming that p and q are both promises and that q is pending, p is resolved to q when either p adopts q or p accepts q. From the .then perspective these are the same, so we'd say p follows q or p is resolved to q. In neither care would p.then fire until q is settled (fulfilled or rejected). However, there's an operational difference between "p adopts q" and "p accepts q" at the .flatMap level: p adopts q does not fire p.flapMap. p accepts q does fire p.flatMap with q as the acceptance value.

# Juan Ignacio Dopazo (7 years ago)

If then() deep flattens, flatMap() only flattens one level and promises assimilate thenables, is branding really necessary?

Juan

# Mark S. Miller (7 years ago)

On the input side of .then and .flatMap, no. On the output side of both .then and .flatMap, depending on what you mean by "branding", yes. If .flatMap's callback returns a non-promise the promise it already returned gets rejected. If .then's callback returns a non-promise, the promise it already returned accepts that non-promise.

# Tab Atkins Jr. (7 years ago)

On Thu, Aug 1, 2013 at 10:33 AM, Juan Ignacio Dopazo <dopazo.juan at gmail.com> wrote:

If then() deep flattens, flatMap() only flattens one level and promises assimilate thenables, is branding really necessary?

The concept of "thenable" is branding. It's just branding with a short, simple string property ("then"), rather than branding with a hard-to-collide string (like __promiseBrand__ or something) or a symbol.

# Juan Ignacio Dopazo (7 years ago)

2013/8/1 Mark S. Miller <erights at google.com>

On the input side of .then and .flatMap, no. On the output side of both .then and .flatMap, depending on what you mean by "branding", yes. If .flatMap's callback returns a non-promise the promise it already returned gets rejected. If .then's callback returns a non-promise, the promise it already returned accepts that non-promise.

That's why I included assimilation in the mix. If then() deep flattens promises there's no difference with assimilation of thenables. Only flatMap() could need to know if something is a "true promise" or not, and even then it could still be more useful for flatMap() to duck type (composition with other flatMappable objects).

# Mark S. Miller (7 years ago)

Between Tab's answer and mine, we see the two issues one might mean by "branding". Anne's clarifying summary at esdiscuss/2013-August/032465 speaks only in terms of "promise-likes". One of the things we need to settle is whether there is one predicate that applies to all the places in this summary that says "if ... promise-like". IMO we need to distinct tests --

  • isPromise for whether p is a promise and
  • isThenable for whether p is thenable.

I would have the output-side tests for both .flatMap and .then be isPromise. I would also have the input side test for .then use isThenable. .flatMap has no input-side test, which is its point. A somewhat separable question is what these two tests are testing.

The first hard reason why we must at least have an isPromise test on the output side of .then is that the promise already returned by .then must adopt this output promise without calling its .then method. Otherwise we lose support for lazy evaluation and for promise pipelining.

If the output side of .then is a non-promise thenable, it is a separate question whether it should be "adopted" by calling its .then method or whether it should be accepted. IMO it should be accepted. The isThenable test indicates something ugly is going on -- assimilation. With this AP2 based design, we can isolate this ugliness to the input side of .then.

The second reason why the two tests need to be separate is that the output side of .flatMap cannot adopt simply by calling the output's .then method, because otherwise you'd often get exactly the recursive unwrapping that .flatMap is trying to avoid. In order to avoid this, it must test whether there is anything it can do to adopt other than calling .then.

# Tab Atkins Jr. (7 years ago)

On Thu, Aug 1, 2013 at 11:04 AM, Mark S. Miller <erights at google.com> wrote:

Between Tab's answer and mine, we see the two issues one might mean by "branding".

No, there's only the one concept. If you mean anything other than "type detection via properties on the instance or its prototype chain", then I don't think you're using the right word. (If I'm wrong, please correct me.)

Detecting thenables via the presence of a function-valued "then" property on the object is, explicitly, branding.

Anne's clarifying summary at esdiscuss/2013-August/032465 speaks only in terms of "promise-likes". One of the things we need to settle is whether there is one predicate that applies to all the places in this summary that says "if ... promise-like". IMO we need to distinct tests --

  • isPromise for whether p is a promise and
  • isThenable for whether p is thenable.

I would have the output-side tests for both .flatMap and .then be isPromise. I would also have the input side test for .then use isThenable. .flatMap has no input-side test, which is its point. A somewhat separable question is what these two tests are testing.

The first hard reason why we must at least have an isPromise test on the output side of .then is that the promise already returned by .then must adopt this output promise without calling its .then method. Otherwise we lose support for lazy evaluation and for promise pipelining.

Right. You can go as far as testing for the existence of a "then" property and verifying that its value is a Function, but you can't call it without breaking these qualities. (For example, you can't call it and verify that it doesn't immediately throw, perhaps because it's a false-positive and doesn't like being passed null/functions as arguments.) If we have a reliable brand unrelated to registering callbacks, that's even better.

If the output side of .then is a non-promise thenable, it is a separate question whether it should be "adopted" by calling its .then method or whether it should be accepted. IMO it should be accepted. The isThenable test indicates something ugly is going on -- assimilation. With this AP2 based design, we can isolate this ugliness to the input side of .then.

Hm, that works for me. It is undetectable whether you do adoption/assimilation on the output side or do wait-for-a-non-promise on the input side, except via measuring timeing/ordering of when .then() is called (which you shouldn't be doing, hopefully). This strategy also makes us somewhat more consistent in behavior when you do a .then().flatMap() chain, between returning from .then() a real promise and a thenable - if you do detection on the output side, the latter case will fully assimilate, while the former will only adopt (one level unwrapping). If you defer detection, then the latter case just accepts, which is closer to the former case's adoption.

The second reason why the two tests need to be separate is that the output side of .flatMap cannot adopt simply by calling the output's .then method, because otherwise you'd often get exactly the recursive unwrapping that .flatMap is trying to avoid. In order to avoid this, it must test whether there is anything it can do to adopt other than calling .then.

Yup, .flatMap() needs to do detection for a real/explicitly-branded promise. (It can't detect for "flatMap-able", because that's meant to be the generic monad operation. Lots of different types of objects can be monads in different ways, so the methods are not compatible between types.)

# Mark S. Miller (7 years ago)

On Thu, Aug 1, 2013 at 11:26 AM, Tab Atkins Jr. <jackalmage at gmail.com>wrote:

On Thu, Aug 1, 2013 at 11:04 AM, Mark S. Miller <erights at google.com> wrote:

Between Tab's answer and mine, we see the two issues one might mean by "branding".

No, there's only the one concept. If you mean anything other than "type detection via properties on the instance or its prototype chain", then I don't think you're using the right word.

This is not a correct summary of the way we've been using "branding". But it is one kind of branding if you wish.

(If I'm wrong, please correct me.)

Done ;)

Detecting thenables via the presence of a function-valued "then" property on the object is, explicitly, branding.

Anne's clarifying summary at esdiscuss/2013-August/032465.htmlspeaks only in terms of "promise-likes". One of the things we need to settle is whether there is one predicate that applies to all the places in this summary that says "if ... promise-like". IMO we need to distinct tests --

  • isPromise for whether p is a promise and
  • isThenable for whether p is thenable.

I would have the output-side tests for both .flatMap and .then be isPromise. I would also have the input side test for .then use isThenable. .flatMap has no input-side test, which is its point. A somewhat separable question is what these two tests are testing.

The first hard reason why we must at least have an isPromise test on the output side of .then is that the promise already returned by .then must adopt this output promise without calling its .then method. Otherwise we lose support for lazy evaluation and for promise pipelining.

Right. You can go as far as testing for the existence of a "then" property and verifying that its value is a Function, but you can't call it without breaking these qualities. (For example, you can't call it and verify that it doesn't immediately throw, perhaps because it's a false-positive and doesn't like being passed null/functions as arguments.) If we have a reliable brand unrelated to registering callbacks, that's even better.

+1

If the output side of .then is a non-promise thenable, it is a separate question whether it should be "adopted" by calling its .then method or whether it should be accepted. IMO it should be accepted. The isThenable test indicates something ugly is going on -- assimilation. With this AP2 based design, we can isolate this ugliness to the input side of .then.

Hm, that works for me. It is undetectable whether you do adoption/assimilation on the output side or do wait-for-a-non-promise on the input side, except via measuring timeing/ordering of when .then() is called (which you shouldn't be doing, hopefully). This strategy also makes us somewhat more consistent in behavior when you do a .then().flatMap() chain, between returning from .then() a real promise and a thenable - if you do detection on the output side, the latter case will fully assimilate, while the former will only adopt (one level unwrapping). If you defer detection, then the latter case just accepts, which is closer to the former case's adoption.

Excellent!

The second reason why the two tests need to be separate is that the output side of .flatMap cannot adopt simply by calling the output's .then method, because otherwise you'd often get exactly the recursive unwrapping that .flatMap is trying to avoid. In order to avoid this, it must test whether there is anything it can do to adopt other than calling .then.

Yup, .flatMap() needs to do detection for a real/explicitly-branded promise. (It can't detect for "flatMap-able", because that's meant to be the generic monad operation. Lots of different types of objects can be monads in different ways, so the methods are not compatible between types.)

Great! I think we continue to converge very nicely.

# Claude Pache (7 years ago)

One more idea: a Promise.register function, which takes a class (i.e. a constructor) C as argument, and whose purpose is to declare that instances of C are to be treated as promises.

Concretely, if the @@isPromise design is retained, that function can be implemented as following:

Promise.register = function(C) {
	C.prototype[@@isPromise] = true
}

But the trick with the symbol is an implementation detail.

# Domenic Denicola (7 years ago)

From: Mark S. Miller [erights at google.com]

No. Assuming that p and q are both promises and that q is pending, p is resolved to q when either p adopts q or p accepts q. From the .then perspective these are the same, so we'd say p follows q or p is resolved to q. In neither care would p.then fire until q is settled (fulfilled or rejected). However, there's an operational difference between "p adopts q" and "p accepts q" at the .flatMap level: p adopts q does not fire p.flapMap. p accepts q does fire p.flatMap with q as the acceptance value.

After being confused on this point for a while, I hashed it out with Tab over IRC (thanks Tab!) and thought I'd share my moment of enlightenment with all involved.

var foreverPending = new Promise(() => {});

var notAcceptedAndNotResolved = Promise.resolve(foreverPending);
var acceptedButNotResolved = Promise.fulfill(foreverPending);

// Neither of them are fulfilled, so the distinction doesn't matter for `then` usage.
notAcceptedAndNotResolved.then(() => console.log("this will never happen (never fulfilled)"));

acceptedButNotResolved.then(() => console.log("this will never happen (never fulfilled)"));

// But it matters for `flatMap` usage.
notAcceptedAndNotResolved.flatMap(() => console.log("this will never happen (never accepted)"));

acceptedButNotResolved.flatMap(() => console.log("this *will* happen"));
# Domenic Denicola (7 years ago)

Er, replace notAcceptedAndNotResolved with resolvedButNotAccepted. X_x