A Challenge Problem for Promise Designers

David Sheets kosmo.zb at gmail.com
Sat Apr 27 19:41:22 PDT 2013


On Sun, Apr 28, 2013 at 1:46 AM, Mark Miller <erights at gmail.com> wrote:
> 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.

But promises aren't thenable? Why are they special?

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

If ((
f : a -> b => a -> Promise<b> if b != Promise<c> for all c
but
f : a -> b => a -> b if b = Promise<c> for all c
) for all f under "then")
is "autolifting"

Then ((
f : a -> Promise<b> => a -> Promise<b> if b != Promise<c> for all c
but
f : a -> Promise<b> => a -> Promise<c> if b = Promise<c> for all c
) for all f under "then")
is "autojoining"

Because "join" : M M t -> M t

(properly, "autolifting" should convert a -> b into Promise<a> ->
Promise<b> and Promise<a> -> Promise<b> into Promise<a> -> Promise<b>
but we can probably ignore this earlier mis-take I made so long as we
can agree on the behavior; "return autoboxing" might have been a
better terminological choice)

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

I agree. We should adopt precise definitions to avoid further
confusion. To that end, I  propose we formally define "flattening" to
be recursively autojoining to a fixpoint.

That is,

assimilation : thenable :: flattening : promise

This is because flattening implies things are, well, flat (one-level
and no more).

> 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?

Let's use P<t> for the promise type constructor and T<t> for the
thenable type constructor.

Suppose we evaulate y = Q(x : P<T<P<T<P<t>>>>>). [which should really
never happen but ignore that, this is for edification]

Does Q simultaneously flatten and assimilate its argument to a
fixpoint? Afaict, yes, because promises are a subset of thenables. Is
y : P<t>?

I agree that Q is also overloaded as an autolifter such that Q : t ->
P<t> for simple t and Q : P<t> -> P<t> for simple t.

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

To do autolifting, you must have a predicate that distinguishes P<t> from T<t>.

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

This assumes that promises are somehow special and beyond construction
by the programmer. This immaculate distinction troubles me because it
immediately closes off the ability to construct systems which are
peers to promises. That is,

promise.then(x => return unblessedPromise()).then(y =>
alert(isUnblessedPromise(y)))

alerts "true" because unBlessedPromise() was lifted into
P<UnblessedPromise<t>> instead of being helpfully unwrapped and then
lifted into Promise<t>. (I should note here that dynamically
dispatching the subsequent "then" to UnblessedPromise<t> is an
interesting possible design decision and probably necessary to
maintain associativity. This could be overridden by return
unblessedPromise().then(Promise.of);)

That is the behavior of the above is identical to the behavior of

promise.then(x => return Promise.of(unblessedPromise())).then(y =>
alert(isUnblessedPromise(y)))

which is quite disappointing.

Ideally, any standardization of this facility would define a simple
object protocol and a set of equivalences of operations over objects
with types in that protocol's class. If a promise system which
seamlessly interoperates with Official Promises cannot be constructed
by a programmer (no object I can construct is true for the isPromise
predicate), we will have lost a great deal. If this occurs, any
JavaScript environment which enforces this blessed/unblessed
distinction will no longer allow developers to construct many
important libraries and compatible variations of promises (e.g. I want
to centrally and conditionally enable logging for all promise
fulfillments).

If you allow interoperability, then it must be decided how to signal
what it *is* to *be* a promise and all promises must exhibit certain
properties. In fact, I would go so far as to say that any /true/
standard should define both the contract of a promise-able and the
specific semantics that its flavor of promise-able implements.

>From this point of view, it is impossible to mandate no
promises-for-promises as there is, by definition, no way to tell if a
given promise is yours or mine. This is an important invariant and
crucial for the future health and extensibility of this feature.

Whether adherence to that contract occurs by checking
isCallable(x.then) or in some other manner, it does not matter (except
for compatibility with existing libraries which use that predicate as
typeclass advertisement). In all circumstances, no recursive
operations over promises or thenables should be mandated. Autojoining
and autolifting are OK and, as you observe, lifting and joining cannot
truly be prohibited. Assimilation or flattening are useful
operation(s) when explicitly invoked; however, it should not occur in
regular use of a promise.

I feel we have made great progress. I hope it is clear now that no
built-in (automatically in, e.g., "then") unwrapping or assimilation
should occur. If that is settled, the only remaining issue is over the
signal to indicate that something is a promise whether built-in or
user-defined.

Best wishes,

David


More information about the es-discuss mailing list