W3C home > Mailing lists > Public > public-script-coord@w3.org > April to June 2013

Re: Promises: Auto-assimilating thenables returned by .then() callbacks: yay/nay?

From: Tab Atkins Jr. <jackalmage@gmail.com>
Date: Fri, 3 May 2013 08:28:53 -0700
Message-ID: <CAAWBYDCBoh9GaRzdPW_uZ0TcujRba11YsuobCwKzL2sPwUiOOA@mail.gmail.com>
To: Domenic Denicola <domenic@domenicdenicola.com>
Cc: Jonas Sicking <jonas@sicking.cc>, "Mark S. Miller" <erights@google.com>, "public-script-coord@w3.org" <public-script-coord@w3.org>
On Thu, May 2, 2013 at 5:38 PM, Domenic Denicola
<domenic@domenicdenicola.com> wrote:
> So, to preface, this entire reply is going to talk about promises-for-promises, not thenable assimilation. Sorry about taking over your well-intentioned thread on thenable assimilation, but I think there have been some points made here that need to be answered.

Since there was apparently confusion on my part about how much
promises-for-promises was resolved (though, I maintain, due partially
to confusing wording ^_^), I'm fine with this.

> From: Tab Atkins Jr. [mailto:jackalmage@gmail.com]
>
>> Can you elaborate on the complexity?  Based on the Futures spec, it seems to be pretty simple.
>
> Not the spec complexity of course; fulfill is simpler than resolve in that sense. The complexity it introduces into the ecosystem, by allowing promises for promises to exist and gum up the works.

What I've tried to argue, apparently convincingly to many, is that
it's hard to accidentally get a nested promise.  You can't do it by
accident.  Thus, I don't see the threat to the "ecosystem".

You start with a promise.  In the .then() callbacks, you return either
a value, or another promise.  Regardless, you end up with a
single-layer promise.  The only way to end up with a nested promise is
to *explicitly* double-wrap it, in which case you either know what
you're doing (and don't want automatic unwrapping), or you have no
idea what you're doing, and an error is much better than papering over
the author's confusion.

>> This is, generically, the ability to compose Futures without having to worry about what's inside of them.  You've come up with one example.
>
> I disagree. This is about a two-stage computation, which should be explicitly separated as such if desired (and it's not even clear it is desired). For example, instead of storing a FunctionFuture directly, the API could store a `{ functionToCompile }` object, so you do
>
> ```js
> indexedDB.get('storedFunction').then(function (storedFunction) {
>   storedFunction.functionToCompile.then(function (compiledFunction) {
>     // use compiledFunction here.
>   });
> });
> ```

I fundamentally don't understand this suggestion.  This is creating an
object whose sole reason for existence is to hold a promise.  It's
creating a nested promise with a single level of useless indirection
in the middle.  Why do that?  It just makes busy-work, and makes it
harder to fully flatten when you *do* want to just say "gimme the
value in the center".

>> The community of functional programmers has shown that, in general, this kind of thing is very often useful, and is captured in the abstract pattern of "monad".
>>
>> Mark has argued against nesting, based on his experience with E, where promises are featureless wrappers that disappear as soon as they're no longer needed.
>
> This again. We have a few problems here:
>
> 1. First, there's a double-standard here: you are arguing based on your experience with functional, strongly-typed languages, and telling us that applies, while at the same time telling us that Mark's experience with E doesn't apply.

I'm saying that because at least part of his experience *doesn't*
apply.  Nesting promises fundamentally doesn't make sense in E,
because as soon as the outer promise fulfills, *it disappears*.
There's no promise left to nest!  As far as the author can tell, it's
now and was always the inner promise, unless of course that has
fulfilled as well, in which case it's just a plain value.

That sort of mechanic makes sense, but JS won't do it.  Thus, we need
to make sure that our mental models include a promise that sticks
around after fulfillment, and that we take full advantage of that
situation.  We can afford to make promises more than just featureless
references.

Hopefully this shows why this isn't just a "double standard" - I'm
trying to point out the real differences here.

> 2. You assume Mark is arguing based on experiences with E. That seems false: both Mark and the Promises/A+ community have worked extensively with *JavaScript* promises, and are telling you that promises-for-promises are bad news.

Based on your previous comments, it *appears* that most of what you
have a problem with are *accidental* nested promises, caused by
mutually non-communicating promise libraries.  This is a *completely*
different subject than purposely nested promises in a single library.

I agree that accidentally nested promises are bad.  I also argue, as I
did above and in several other places, that it's very hard to
accidentally nest native promises.

> 3. It would be helpful if you established your credibility in the area of functional, strongly-typed languages, so that we could be sure you were speaking from experience instead of just repeating what has appeared elsewhere. Just as a baseline, mind; I'm not accusing you of this, but just saying it would be helpful to set the stage for discussion so that we know where everyone is coming from.

I would hope that my arguing so far shows that I know what I'm talking
about.  If I must show my papers, though, I've been programming in
Lisp since I was in high school, and wrote a lot of monadic code
myself as I learned about it in Haskell (in addition to some fun
arrow-based stuff, which seemed *so* much simpler when I actually
implemented it, rather than trying to understand the category-theory
descriptions).

Note that Lisp is a dynamic strongly-typed language.  Monads work just
fine - you don't get some type inference niceties for free, but that
is only a minor convenience.  JS is identical to Lisp in this regard
(and in many other ways).

> 4. I contend that solutions that work in strongly-typed, functional languages do not transfer well to JavaScript. There are a myriad of technical issues here we could discuss, from the existence of `throw`, to the possibility of returning different types, to side effects, but let's instead focus on the actual experimental evidence. Namely, every time I ask for an example of these hypothetical monad libraries in JS being used extensively, I get nothing but echoes. This indicates nobody is using such libraries in JavaScript extensively to the extent they are using promises, so perhaps we should be thinking about how promises work instead of how monads work?
>
>    It might be the case that we are simply too early, and there is a JS monad renaissance in the wings. I sincerely doubt it, but even if that did happen, we'd need to wait at least a few years to see the kind of widespread use of monads as we do of promises. Maybe then we'd add a `flatMap` function to whatever promise standard exists at the time, to pave the existing cowpath as we are doing with `then` and promises.
>
>   In short, you keep trying to conflate promises and monads, but it really just comes across as you trying to piggyback a related-but-different concept on top of the one we are all discussing. Like some sort of congressional rider bill, really. Let it go?

None of those issues matter in the slightest to the usefulness of the
monad abstraction.  Monad is just a typeclass, identical to "iterable"
or "array-like" or "mappable" or a host of other abstractions that JS
classes use, either explicitly or implicitly.  Don't fetishize "monad"
as something magical and only applicable to crazy languages like
Haskell - it's just a funky name for the concept of chaining a value
in some context, a trivial (though extremely useful) generalization of
the "functor"/"mappable" abstraction that JS already has for all of
its relevant built-ins (all the container types).

What I've been *trying* to do this entire time is find out *why*
native nested promises are harmful.  Every. Single. Person. who's
tried to tell me that nested promises are harmful so far, has ended up
realizing that they were talking about assimilation, or non-JS
promises with weird language semantics, or programmer bugs that would
benefit from causing an error, rather than having the
excessive-wrapping bug ironed over.

Seriously, please read the thread
<http://lists.w3.org/Archives/Public/public-script-coord/2013AprJun/0279.html>
and tell me if a case was missed!  I kept digging for them, and nobody
has been able to give me one so far.  :/

> 5. You have never address the problem of the identity function. If you cannot create promises for promises, then `var p2 = p.then(x => x)` is always an identity transformation; `p2` is always fulfilled with the same value as `p`. However, given `var f = Future.accept(Future.accept(5))`, you have that `var f2 = f.then(x => x)` is no longer an identity transformation; `f2` is fulfilled with `5`, whereas `f` is fulfilled with `Future.accept(5)`. To me this is an extremely clear example of how you are trying to port over semantics from monads to promises that do not fit with the promise framework, and more generally trying to port over semantics from strongly-typed languages to JavaScript, where you can't enforce the return type of the `onFulfilled` callback.

I'm confused - what was I supposed to address about identify
functions?  If I missed a direct question from you in an earlier
thread, I apologize.

The identity function for monad callbacks is their lifting operator.
In this case, that's Future.accept().

The normal identity function (x=>x) is useful for mappables, which
promises *kinda* are, but through magic that interferes with
unconditional mapping, as you demonstrate.

> 6. Finally, related to #5, having these unfortunate entities in the system requires guarding against them. I'd encourage you to take a look at Mark's latest smart contracts paper, and consider what would happen to the workings therein if promises-for-promises started flowing through the system. The identity function breakage certainly, but I would imagine much more.

I did.  Mark brought up an example in his contracts paper in a much
earlier thread, and as far as I could figure out, it had nothing to do
with nested promises - it was about a value which might be a plain
value or a promise, and wanting to deal with as simply as possible.
In E, that's just a matter of calling Q() with the value.  In the
Futures spec, it's exactly the same - call Future.resolve() on the
value.

Again, an *accidental* nested promise would indeed be annoying and
bad.  But I challenge you to create one!  "Accidental nested promises"
is a boogeyman that is very rare in reality, if you have a
properly-functioning abstraction (which we do).

>> It occurs to me now that Domenic may have been doing the same "nested promises can't exist, so of course you (trivially) don't need recursive flattening for them" thing that Mark did earlier in this thread, in which case >_<.
>
> Somewhat. I find the issue of what happens to nested promises very uninteresting, since they will hopefully not exist. I admit that if the monad camp does push them through the W3C, then we'll have to talk about their semantics, but I'd rather bite that bullet when we get shot with it (or something).

Okay.  I'd appreciate if, when your answer to a question is because
the question itself is moot, that you say so, so as to avoid confusion
with people who don't believe it's moot.  ^_^

~TJ
Received on Friday, 3 May 2013 15:29:40 UTC

This archive was generated by hypermail 2.3.1 : Tuesday, 6 January 2015 21:37:49 UTC