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

Re: A Challenge Problem for Promise Designers

From: Mark Miller <erights@gmail.com>
Date: Sat, 27 Apr 2013 17:46:46 -0700
Message-ID: <CAK5yZYgdvexmAhdsixxxBq-yT7rVJObDnv79UzSu=BonecRYfw@mail.gmail.com>
To: David Sheets <kosmo.zb@gmail.com>
Cc: "Mark S. Miller" <erights@google.com>, es-discuss <es-discuss@mozilla.org>, "public-script-coord@w3.org" <public-script-coord@w3.org>, Ron Buckton <rbuckton@chronicles.org>, David Bruant <bruant.d@gmail.com>, "Tab Atkins Jr." <jackalmage@gmail.com>, Dean Tribble <tribble@e-dean.com>
I am worried that we're again separated by a common terminology more than
we are by an actual technical disagreement. I am arguing against an
unconditional lift operation that would make a promise-for-promise. Or at
least seeking to provoke someone to provide a compelling example showing
why this is useful[1]. What all the recent messages seem to be arguing for
is the existence of a non-assimilating and perhaps a non-unwrapping
lift-ish operation, so that one can make a promise-for-thenable. I have no
objection to being able to make a promise-for-thenable.

The clearest sign to me of the potential for misunderstanding is the use of
the term "flattening" for unwrapping of thenables. To be clear,
"flattening" is about promises -- it is just the conditional autolifting
seen from the other side (as I now understand the term autolifting --
thanks). "unwrapping" is what happens to thenables. "Assimilation" is
recursive unwrapping of thenables. I understand that it can be difficult to
keep these distinctions straight. I wouldn't be surprised if I've been
sloppy myself with these terms earlier in these threads. But now that we're
zeroing in, these fine distinctions matter.

As I demonstrated somewhere in one of the Promises/A+ threads, I don't
think it is practical to prohibit these promises-for-thenables anyway. As
an example, let's take Q's use of Q as an allegedly fully assimilating
lift-ish function. Like an autolifter Q(promise) returns that promise. And
Q(x) where x is neither a promise nor a thenable, returns promise-for-x.
The controversial part -- which I fully sympathize with since it is a dirty
hack -- is that Q(thenable) assimilates -- it does recursive unwrapping
(NOT "flattening") of the thenable. Assimilation aside, Q is an autolifter,
so I'll can it an "assimilating autolifter". Assume a system is which this
Q function and .then are the only lift-ish operations, and that .then is
also an assimilating autolifter. What guarantee is supposed to follow?

Assuming that the thenable check is if(typeof x.then === 'function'),
intuitively, we might think that

    Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));

should never alert 'function'. But this guarantee does not follow:

    var p = {};
    Q(p).then(shouldntBeThenable => alert(typeof shouldntBeThenable.then));
    p.then = function() { return "gotcha"; };

This is a consequence of assimilation being a dirty hack. The notion of a
thenable is only marginally more principled than the notion of array-like.
It is not a stable property. Above, p became a thenable after Q already
judged it to be a non-promise non-thenable and hence returned a
promise-for-p. This promise scheduled a call to the callback before p
became a thenable, but p became a thenable before the callback got called.
Alleged guarantee broken.

Thus, no fundamental guarantee would be lost by introducing an expert-only
operation that does autolifting but no unwrapping. This would make
convenient what is possible anyway: promises-for-thenables. But this is
*not* an argument for introducing a full lifting operation. Introducing
that would break an important guarantee -- that there are no
promises-for-promises. Without full lifting, promises-for-promises remain

I leave it to monad fans and/or haters of assimilation to suggest names for
this convenient operation, a non-unwrapping autolifter. I'm confident that
if I tried to name it, I'd only cause more confusion ;).

[1] FWIW, if there's interest, I can provide several examples where a
promise-for-promise is useful, but I know of no compelling example. The
utility of the examples I have in mind are not worth the cost of
introducing this possibility of a promise-for-promise.

On Sat, Apr 27, 2013 at 11:07 AM, David Sheets <kosmo.zb@gmail.com> wrote:

> On Sat, Apr 27, 2013 at 6:05 PM, Mark S. Miller <erights@google.com>
> wrote:
> > On Sat, Apr 27, 2013 at 9:55 AM, David Sheets <kosmo.zb@gmail.com>
> wrote:
> > [...]
> >>
> >> I think the major point of confusion in these discussions is the
> >> result of the framing of the discussion in terms of "flattening". I
> >> believe most beneficial viewpoint is that of "autolifting".
> >>
> >> That is, the exceptional case is not when the function argument of
> >> "then" returns a Future+ that gets "flattened" but rather when the
> >> function argument of "then" returns a non-Future that gets
> >> automatically lifted into a Future.
> >>
> >> This change in perspective is non-obvious because in many of these
> >> APIs there is no succinct lifting operation to make a Future from
> >> another value. This is a major reason why something like Future.of
> >> (Future.accept) is important.
> >
> > I was following you until this last paragraph. As you define autolifting
> in
> > the first two paragraphs, Q(x) would be an autolifting operation. It has
> the
> > signature:
> >
> >     promise<t> -> promise<t>
> > or
> >     t -> promise<t> // if t is not itself a promise type
> >
> > Are you distinguishing "autolifting" vs "lifting"? If so, why do you
> think
> > it is important or desirable to provide a lifting operation (as opposed
> to
> > an autolifting operation)?
> Yes. Autolifting is conditional on promise-ness. Lifting is fully
> parametric.
> If the standard uses autolifting instead of recursive flattening, many
> of the headaches with "thenables" go away and we gain enormous
> flexibility in future interoperation with the spec.
> For instance, if your code might manipulate objects which have
> callable "then" fields but which don't subscribe to the promises spec,
> it is safest to always use:
> return Promise.of(myMaybeNonPromiseThenable);
> This greatly reduces the criticality of the "is this a promise?"
> predicate because in most cases you will simply return a non-thenable
> (autolifted) or a promise-like thenable and not care. In those cases
> where you wish to put non-promise thenable inside of a promise or
> *don't know if someone else will want to*, the explicit use of the
> lifting operation lets you avoid autolifting/flattening.
> This massively simplifies the protocol between the promises spec and
> those values it encapsulates by only ever making a single assumption
> that then-returned thenables are promise-like but their contents are
> *totally opaque*.
> I believe this design results in the maximal flexibility and safety
> for the platform by supplying a handy autolifting "then" while also
> allowing people to easily subscribe to promise interaction (by
> then-returning a thenable), defend their thenables from magical
> unwrapping (by explicitly using Promise.of), and write completely
> polymorphic code.
> With this design, in the most common case, developers won't have to
> use Promise.of. Perhaps the only common use will be in starting a
> promise chain from a constant:
> var promise;
> if (just_use_a_constant) { promise = Promise.of(6); } else { promise =
> getAsyncValue(); }
> promise.then(function (x) { return x*2); });
> While those who translate code from other languages, target their
> compilers to JS Promises, write polymorphic libraries, use
> non-promises with callable "then" fields, or invent new interoperable
> promise-like (but distinct) semantics won't have to worry about
> hacking around recursive unwrapping.
> To me, having some standard for promise-like objects in the platform
> seems very fundamental for handling asynchrony, ordering, failure,
> need, and probability. If we consider promise-like objects as
> fundamental, we should investigate the properties of their operations:
> With recursive flattening "then" operation, the time complexity of
> "then" is O(n) with n the number of nested promise-like objects.
> With autolifted "then" operation, the time complexity of "then" is O(1).
> Here, I am using time complexity as a proxy for the mental complexity
> of the operation and not as a proxy for execution performance
> (recursive unwrapping is usually of static depth as we have seen). You
> can see that not only does the recursive flattening involve a hidden
> loop that the advanced programmer must reason about but also invokes
> the notion of "promise-like object" which, as we have seen, leads to
> all sorts of tangents regarding how to characterize the property of
> promise-ness and still maintain clarity, safety, extensibility, and
> ease-of-use.
> I hope this explanation satisfies you. If not, I am more than happy to
> answer any questions you may have about this approach.
> Warm regards,
> David Sheets

Text by me above is hereby placed in the public domain

Received on Sunday, 28 April 2013 00:47:13 UTC

This archive was generated by hypermail 2.4.0 : Friday, 17 January 2020 17:14:13 UTC