Deprecating Future's .then()

# Mark S. Miller (12 years ago)

[+es-discuss]

I just realized that this thread has occurred so far only on the wrong lists. Please let's proceed from here only on es-discuss. This is a language issue, not a browser issue. Let's please stop splitting the discussion between two communities.

# Mark S. Miller (12 years ago)

[-public-script-coord, -www-dom]

On Tue, Jun 4, 2013 at 8:42 AM, Mark S. Miller <erights at google.com> wrote:

[+es-discuss]

I just realized that this thread has occurred so far only on the wrong lists. Please let's proceed from here only on es-discuss. This is a language issue, not a browser issue. Let's please stop splitting the discussion between two communities.

On Tue, Jun 4, 2013 at 8:32 AM, Mark S. Miller <erights at google.com> wrote:

On Tue, Jun 4, 2013 at 7:34 AM, Domenic Denicola < domenic at domenicdenicola.com> wrote:

On Tue, Jun 4, 2013 at 9:48 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, Jun 4, 2013 at 8:55 AM, Sam Tobin-Hochstadt <samth at ccs.neu.edu> wrote:

Thinking about this more, I'm now unsure why both fulfill and resolve are needed given the semantics of .chain() and .then() described below.

In particular, if .then() chains recursively before calling the callback, then there's no difference between:

Future.resolve(x).then(v => ...)

and

Future.fulfill(x).then(v => ...)

even when x is a promise. The only way to observe this is with .chain().

Thoughts?

I'm just going to try to repeat what you said here to make sure I understand.

Promise.resolve(val) creates a promise of val, regardless of whether val is a promise, has a callable then property, or anything like that. (In that sense it is equivalent to Future.accept() today.)

promise.then() keeps unwrapping promise's internal value until it no longer has a callable then property at which point it invokes the relevant callback passed to promise.then(). (Exact algorithm TBD after broader agreement.)

promise.chain() invokes its relevant callback with promise's internal value.

promise.then() and promise.chain() return value (newPromise) is resolved with the return value of their callbacks after it has been unwrapped once.

In general, this approach is extremely interesting. The shift from focusing on promise fulfillment and being in the three states of pending, fulfilled, and rejected to focusing on promise resolution and being in the two "fates" of unresolved and resolved is a big difference. But it is probably a win as it ends up eliminating the state concept almost entirely, making it just an emergent way of describing what happens with then.

Agreed. This is a great direction. Thanks, Sam!

Given this direction, I think the one operation that serves as both Promise.resolve and Promise.fulfill should be the previously suggested Promise.of.

One point I am not entirely clear on is why there is any unwrapping of return values, as in the last step Anne describes. For consumption with then it seems to make no difference. I assume this is to match flatMap or bind semantics from other languages, so that if you use chain exclusively you match Haskell/Scala/et al. semantics?

If you don't unwrap1 at all, i.e., go with a .map-like treatment of return values, rather than a .flatMap-like treatment, the difference is unobservable to those who use .then exclusively, and the semantics seems simpler, so this would seem to be a win. But the storage costs would be * HUGE*! Since the implementation can't in general tell whether a promise will be observed with .chain or .then later, it would have to preserve each level of nesting for .then calls nested in .then calls. This would lose the flattening property that corresponds to being insensitive to how many levels deep in a function call chain a value was returned from. The normal tail-recursive promise loop pattern < strawman:async_functions#reference_implementation> would need to accumulate a level of nesting per iteration of the loop.

I also think the name "chain" is pretty confusing, especially in light of already-existing terminology around promise chaining 1. Is there precedence for it in other languages? flatMap seems clearest to me so far.

I agree. This terminology will lead to confusion: "To do promise chaining, use .then. The .chain method doesn't support promise chaining." As for .flatMap, I am indifferent, since I'm planning to avoid it myself. I leave it to its advocates to suggest something unconfusing.

1 I am always worried though when people use the term "unwrapping" as I don't know what they mean. Does this mean flattening, assimilating, both, or something else? What I mean here is to so one level of flattening. As for whether .then should also do one level of assimilation if it sees a non-promise thenable, I could go either way, but prefer that it should not. The promise-cross-thenable case should be sufficiently rare that the cost of the extra bookkeeping should be negligible.

I am making here only an argument that .then's result behavior should be flatMap-like rather than .map-like. As for which of these the .chain camp prefers for .chain's result behavior, I am neutral. But if they choose .map-like, they'd be able to avoid the ugly assimilation hack.

# Mark S. Miller (12 years ago)

(For the es-discuss record)

---------- Forwarded message ---------- From: Mark S. Miller <erights at google.com>

Date: Mon, Jun 3, 2013 at 2:20 PM Subject: Re: Deprecating Future's .then() To: "Tab Atkins Jr." <jackalmage at gmail.com>

Cc: Anne van Kesteren <annevk at annevk.nl>, Sean Hogan <

shogun70 at westnet.com.au>, "www-dom at w3.org" <www-dom at w3.org>, "

public-script-coord at w3.org" <public-script-coord at w3.org>, Alex Russell <

slightlyoff at google.com>

+1 on all counts regarding mechanism. As for how happy the conclusion is, I remain skeptical. But given that we cannot get consensus in TC39 without support for Promise.fulfill, as was obvious in the London/May meeting, I do agree that AP2 as Tab describes here is our best option. We'll see how happily the two styles actually co-exist in practice.

I'll hazard a prediction: the Q style continue to dominate. The .fulfill/.flapMap means of tunneling a fully parametric value through Q-style abstractions will prove unsatisfactory in practice, and people will instead use the {value: x} style. The Q-style folks will be happy in that they can proceed ignoring the existence of .fulfill and .flatMap, as Tab correctly observes.

The .flatMap-style folks will be unhappy with the unreliability of using .fulfill/.flatMap to tunnel through Q-style abstractions. Eventually they will either a) treat the two styles as two different universes that do not interoperate, and so should have simply been disjoint abstractions, or b) give up and use the {value: x} style wrapper to tunnel through promise abstractions reliably. In either case, in retrospect it will have turned out to be a mistake to try to support both styles with the one API. But a mistake we will mostly be able to ignore. We've lived with worse.

# Tab Atkins Jr. (12 years ago)

On Wed, Jun 5, 2013 at 12:51 AM, Mark S. Miller <erights at google.com> wrote:

I am making here only an argument that .then's result behavior should be flatMap-like rather than .map-like. As for which of these the .chain camp prefers for .chain's result behavior, I am neutral. But if they choose .map-like, they'd be able to avoid the ugly assimilation hack.

Given that .chain is just a proposed name for the monadic operation, which could also be called .flatMap, obviously the return value's behavior should be flatMap like.

I don't understand what the "ugly assimilation hack" is. Is that looking for non-promise thenables? There's no particular reason that .chain needs to do assimilation, particularly if we add a Promise.from() which does assimilation for us (converting the thenable into a real Promise)

I agree. This terminology will lead to confusion: "To do promise chaining, use .then. The .chain method doesn't support promise chaining."

I have no idea what this line is talking about. Of course .chain supports promise chaining. Promise chaining is the entire point of the monad. It's lines like this that make me unsure if we're just using very different definitions for the same words for some reason, or if there's a real gap of understanding on either or both sides.

# Tom Van Cutsem (12 years ago)

2013/6/4 Tab Atkins Jr. <jackalmage at gmail.com>

On Wed, Jun 5, 2013 at 12:51 AM, Mark S. Miller <erights at google.com> wrote:

I am making here only an argument that .then's result behavior should be flatMap-like rather than .map-like. As for which of these the .chain camp prefers for .chain's result behavior, I am neutral. But if they choose .map-like, they'd be able to avoid the ugly assimilation hack.

Given that .chain is just a proposed name for the monadic operation, which could also be called .flatMap, obviously the return value's behavior should be flatMap like.

Here's another argument I haven't heard before:

.flatMap seems like an excellent name because it immediately provides a clue as to what it does without having to look up any docs at all. Given that as a JS dev you know array.map(), you'll have some intuition that .flatMap() does what map() does, with flattening (it's a pity we don't have array.flatten() ).

Similarly, Promise.of(val) is way clearer than Promise.accept/fulfill, if you compare it to Array.of(val) (not sure we eventually agreed to add such a method, but I think to most JS devs it will be entirely clear what this does, the array-equivalent of Promise.of(val) is like writing [val])

In general, while arrays in JS are not truly monadic, they are sufficiently monadic to serve as inspiration for the promise operations.

Bikeshedding names aside, I can agree to the semantics as summarized by Tab.

# Anne van Kesteren (12 years ago)

On Tue, Jun 4, 2013 at 4:42 PM, Mark S. Miller <erights at google.com> wrote:

I just realized that this thread has occurred so far only on the wrong lists. Please let's proceed from here only on es-discuss. This is a language issue, not a browser issue. Let's please stop splitting the discussion between two communities.

That's why public-script-coord is used, which includes both. And for now it's implemented above the language-level, so excluding those not subscribed to es-discuss as you did in your next emails is poor form.

-- annevankesteren.nl

# Anne van Kesteren (12 years ago)

On Tue, Jun 4, 2013 at 4:32 PM, Mark S. Miller <erights at google.com> wrote:

Given this direction, I think the one operation that serves as both Promise.resolve and Promise.fulfill should be the previously suggested Promise.of.

I think you still want Promise.resolve because it makes sense with Promise.reject and the instance methods on PromiseResolver. Promise.of would be a good alias to have.

I am always worried though when people use the term "unwrapping" as I don't know what they mean. Does this mean flattening, assimilating, both, or something else? What I mean here is to so one level of flattening. As for whether .then should also do one level of assimilation if it sees a non-promise thenable, I could go either way, but prefer that it should not. The promise-cross-thenable case should be sufficiently rare that the cost of the extra bookkeeping should be negligible.

Sorry for the confusion. I thought you guys had already settled on accepting what the DOM standard currently defines (no branding checks of any kind, just use a callable then property, if any). If that's not the case, writing out the desired behavior for then()/flatMap() here would help.

-- annevankesteren.nl

# Mark S. Miller (12 years ago)

We are arguing about enough other issues that -- since this seems to be controversial as well -- to avoid an avoidable argument, I will include all three lists as you do here. However, for the record, I think this is silly. There's nothing about this issue that is browser specific. The fact that it is currently "implemented above the language-level", meaning I assume the language implementation, is irrelevant. Many linguistic abstractions are implemented first as libraries. This is a victory when it happens, as it means the underlying abstraction mechanisms were sufficient. Over time, some elements of some of these libraries become blessed as part of the language. This is as it should be.

What about any of this is specific to the browser is beyond me.

# Anne van Kesteren (12 years ago)

On Wed, Jun 5, 2013 at 3:17 PM, Mark S. Miller <erights at google.com> wrote:

We are arguing about enough other issues that -- since this seems to be controversial as well -- to avoid an avoidable argument, I will include all three lists as you do here. However, for the record, I think this is silly. There's nothing about this issue that is browser specific. The fact that it is currently "implemented above the language-level", meaning I assume the language implementation, is irrelevant. Many linguistic abstractions are implemented first as libraries. This is a victory when it happens, as it means the underlying abstraction mechanisms were sufficient. Over time, some elements of some of these libraries become blessed as part of the language. This is as it should be.

What about any of this is specific to the browser is beyond me.

The reason the way it's implement is relevant is because it involves a different set of people, who are not all subscribed to es-discuss, yet are affected by the outcome of this discussion.

On top of that, the specification they're implementing is primarily discussed on @w3.org lists. And I'd like to update that specification as soon as possible with this newer design (and names) as it seems to have broader consensus than the current one.

-- annevankesteren.nl

# Mark S. Miller (12 years ago)

On Wed, Jun 5, 2013 at 3:37 AM, Anne van Kesteren <annevk at annevk.nl> wrote:

On Tue, Jun 4, 2013 at 4:32 PM, Mark S. Miller <erights at google.com> wrote:

Given this direction, I think the one operation that serves as both Promise.resolve and Promise.fulfill should be the previously suggested Promise.of.

I think you still want Promise.resolve because it makes sense with Promise.reject and the instance methods on PromiseResolver. Promise.of would be a good alias to have.

Wow. In writing a response to this I ended up reversing myself. I realize that I no longer agree that one operation serves both purposes. I agree with Sam that Promise.resolve no longer needs to do recursive flattening or assimilation. But unlike Promise.fulfill, it does need to do one level of flattening. The reason is the same as the reason that .then (and presumably .chain whatever it is called) needs to do one level of flattening (.flatMap-like) on its result -- the allocation cost of doing otherwise is prohibitive. The common defensive promise programming pattern is

Q(p).then(...)

or more generally,

....Q(p)....

when p is received from a possibly untrusted source, such as any client of a defensively consistent abstraction, to ensure one is operating on a genuine promise and protected from plan interference attacks. If we have both Promise.resolve and Promise.fulfill, then Q as a function would be equivalent to Promise.resolve and all would be happy. But if we have only Promise.fulfill (whether spelled Q or Promise.of or whatever) and it is used instead in such patterns, then, because of the presence of .chain, it would always have to allocate a new promise even when p is already a promise.

For the same reason, promiseResolver.resolve also needs to do one level of flattening.

I am always worried though when people use the term "unwrapping" as I don't know what they mean. Does this mean flattening, assimilating, both, or something else? What I mean here is to so one level of flattening. As for whether .then should also do one level of assimilation if it sees a non-promise thenable, I could go either way, but prefer that it should not. The promise-cross-thenable case should be sufficiently rare that the cost of the extra bookkeeping should be negligible.

Sorry for the confusion. I thought you guys had already settled on accepting what the DOM standard currently defines (no branding checks of any kind, just use a callable then property, if any). If that's not the case, writing out the desired behavior for then()/flatMap() here would help.

No. We still need the brand check. I will write the desired behavior soon -- hopefully tonight.

# Rick Waldron (12 years ago)

On Wed, Jun 5, 2013 at 5:29 AM, Tom Van Cutsem <tomvc.be at gmail.com> wrote:

2013/6/4 Tab Atkins Jr. <jackalmage at gmail.com>

On Wed, Jun 5, 2013 at 12:51 AM, Mark S. Miller <erights at google.com>

wrote:

I am making here only an argument that .then's result behavior should be flatMap-like rather than .map-like. As for which of these the .chain camp prefers for .chain's result behavior, I am neutral. But if they choose .map-like, they'd be able to avoid the ugly assimilation hack.

Given that .chain is just a proposed name for the monadic operation, which could also be called .flatMap, obviously the return value's behavior should be flatMap like.

Here's another argument I haven't heard before:

.flatMap seems like an excellent name because it immediately provides a clue as to what it does without having to look up any docs at all. Given that as a JS dev you know array.map(), you'll have some intuition that .flatMap() does what map() does, with flattening (it's a pity we don't have array.flatten() ).

Similarly, Promise.of(val) is way clearer than Promise.accept/fulfill, if you compare it to Array.of(val) (not sure we eventually agreed to add such a method,

To clarify for Tom and anyone else following that was unsure: Array.of(...items) has been spec'ed and is located at 15.4.2.3 of rev15 (June)