Killing `Promise.fulfill`

Mark S. Miller erights at
Tue Aug 20 20:55:41 PDT 2013

On Tue, Aug 20, 2013 at 11:04 AM, Tab Atkins Jr. <jackalmage at>wrote:

> On Tue, Aug 20, 2013 at 9:18 AM, Mark S. Miller <erights at>
> wrote:
> > To answer this precisely, we need good terminology to distinguish two
> levels
> > of abstraction: The distinctions observable to the AP2.flatMap programmer
> > and the coarser distinctions observable to the AP2.then programmer. Let's
> > start ignoring thenables and considering only promises-vs-non-promises.
> > Let's also start by ignoring rejection.
> >
> > At the AP2.flatMap level,
> >   * for a promise p and an arbitrary value v, p may accept v. p is then
> in
> > the "accepted" state.
> >   * for a promise p and a promise q, p may adopt q. p is then in the
> > "adopting" state.
> > Putting these together, we can also say
> >   * for a promise p and an arbitrary value v, p is resolved to v if p
> either
> > accepts v or adopts v. p is then in the "resolved" state.
> >
> > p2 = p1.flatMap(v1 => q2)
> >
> > means, if p1 is accepted, then v1 will be what it has accepted.
> >
> > If q2 is a promise, then p2 adopts q2.
> >
> > p2.flatMap(...) fires as a result of acceptance but not adoption. If q2
> > accepts, then p2 likewise accepts and p2.flatMap fires by virtue of this
> > acceptance.
> >
> > At the P2.then level
> >   * for a promise p and a non-promise v, p may be fulfilled with v. p is
> > then in the fulfulled state.
> >   * for a promise p and a promise q, p may follow q. p is then in the
> > following state.
> >   * Until a promise p is either fulfilled or rejected, it is pending.
> > Putting these together, we can also say
> >   * for a promise p and an arbitrary value v, p is resolved to v if
> either p
> > is fulfilled with v or p follows v. p is then in the "resolved" state.
> >
> > p4 = p3.then(v3 => v4)
> >
> > means, if p3 is fulfilled, then v3 will be what p3 is fulfilled with.
> >
> > p4 is resolved to v4. If v4 is a promise, then p4 follows v4. Else p4 is
> > fulfilled with v4.
> >
> > p4.then fires as a result of fulfillment but not following. If p4
> follows v4
> > and v4 fulfills, then p4 likewise fulfills and p4.then fires by virtue of
> > this fulfillment.
> >
> > Notice that "resolved" is the same states at each level, even though
> these
> > states are described differently. That is why we can use the same term at
> > both levels. Likewise, the concept of "unresolved" is meaningful at both
> > levels.
> Argh, I knew this would turn into another confusing terminology
> discussion.  ^_^

Indeed ;).

I think this is because you more naturally think at the AP2.flatMap level
of abstraction and derive AP2.then level concepts from that. And vice versa
for me. That's why I tried to clearly lay out and distinctly name the
concepts relevant at each level.

> I'm not quite getting this.  Why are you using "resolved" in this way?

Because it corresponds to how "resolved" has historically been used in
Promses/A+ ever since the Promises/A+ distinguished "resolved" vs
"settled". It also describes what .resolve does. Your proposed meaning of
.resolve if what I'm calling .accept. To the AP2.then observer, this also
does the job historically associated with .resolve, but at a prohibitive
storage cost for .then oriented patterns. Using .accept for .resolve would
be much like using a non-tail-recursive language implementation to execute
algorithms written assuming tail recursion optimization.

The relationship between the two is:

      resolve(v) => { isPromise(v) ? adopt(v) : accept(v) }

except that I am not proposing that an explicit "adopt" method be added to
the API.

>  It doesn't seem to map to a useful state for either mode, since
> you're munging together the case where v4 is a value (p4 can call its
> callbacks) and where v4 is a promise (p4 maybe can't call its
> callbacks yet, or ever, depending on v4's state).

I'm just restating the semantics I thought we agreed on. From the AP2.then
perspective p4 resolves to v4. From the AP2.flatMap perspective, if v4 is a
promise, p4 adopts v4. Otherwise p4 accepts v4.

The only alternative I see is that p4 always accepts v4. This would
accumulate an explicit layer of wrapping for each level of then-return,
since these layers would need to be observable by .flatMap (unless the
implementation can prove that p4 will never be observed with .flatMap,
which is unlikely to be common).

Regarding the calling of p4.then callbacks, your summary is correct: if v4
is a non-promise, then p4 fulfills to it and p4.then can call its
callbacks. If v4 is a pending promise, then p4.then cannot yet call its
callbacks. What am I missing? What is being munged?

>  You're also munging
> together the case where q2 is pending vs not-pending, which again
> means that either p2 can call its callbacks or not.

I'm using the term "pending" at the AP2.then level, to mean "not fulfilled
or rejected". The similar concept at the AP2 level isn't something we've
previously named, but it means "not accepted". Here I will use "unaccepted".

So I don't know what you mean by munging. I thought we agreed that p2
adopts q2, and while q2 is unaccepted, p2 is unaccepted as well and
p2.flatMap cannot call its callbacks. Once q2 becomes accepting, then p2
becomes accepting as well and p2.flatMap can call its callbacks. Are we in

> In my email, and I think Domenic in his, I'm trying to nail down some
> terms that map to useful states, capturing observable distinctions in
> behavior:
> "resolved" means a promise contains a value - it's no longer pending.

Domenic clarified that the modern term for "no longer pending", i.e.,
fulfilled or rejected, is "settled". Perhaps this is the source of
confusion? In E, Waterken, perhaps AmbientTalk, and in historically earlier
versions of the Promises/A+ spec, we used to say "resolved" for what we now
call "settled". We changed this terminology precisely because of the
conflict that the .resolve operation did not cause a promise to be what we
had called "resolved" and now call "settled".

> Your p2 is resolved only when q2 becomes resolved, due to adoption
> semantics.

Ok, this hypothesis fits. It is true that

      p2 is settled only when q2 becomes settled, due to adoption

However, the original is not true. Since .resolve either adopts or accepts,
p2 is resolved as soon as it adopts q2.

OTOH, the previous hypothesis that your "resolved" is my "accepted" also

      p2 is accepted only when q2 becomes accepted due to adoption

I can guess which rewrite better fits what you're trying to say, but I'll
let you clarify.

>  (If you were to put q2 directly into another promise, via
> `p2 = Promise.resolve(q2)`, then p2 would be resolved.


>  Adoption
> semantics flatten one level, but `Promise.resolve()` isn't adopting.)

This is true for .accept.

> A promise is "resolved" when it would call its flatMap() callbacks.

Again true for "accepted".

> "fulfilled", taken from Promises/A+, means a promise contains a
> non-promise value, or contains a fulfilled promise.

Ignoring thenables as we're doing here, yes.

>  Your p4 is only
> fulfilled if v4 is a non-promise value, or is a fulfilled promise.


> So, a promise starts out "pending", becomes "resolved", and then
> becomes "fulfilled".  This ordering is always preserved, though some
> states might happen at the same time.

Not quite. A promise starts out pending an unresolved.  If p5 is resolved
to p6 and p6 is pending, then p5 is both pending and resolved.

> If necessary, we can come up with distinct terms for "not resolved"


> and "not fulfilled",

"pending" === "not fulfilled and not rejected"

> since a promise can be resolved but not
> fulfilled.  (This is exactly the state that p4 is in if v4 is a
> non-fulfilled promise.)

>  "Not fulfilled" = "pending" (Promises/A+
> meaning)

yes, ignoring rejected as we are doing here.

> and "not resolved" = "super pending"? ^_^


> We could use the terms differently than what I've defined here, but why?

I think you're missing one distinction, as you're using "resolved" for what
I'm calling "accepted" and you have no name for what I'm calling
"resolved". By presuming that the accept operation is used for cases that
Promises/A+ uses the resolve, you impose prohibitive storage costs.

> > I'm not so much concerned with the static .resolve method, since the
> extra
> > storage cost for the static method is negligible.

Replying to myself here, I retract that statement of non-concern, since the
static .resolve method would be the std method to be used the way the Q
function is currently used. The static .resolve method's behavior, in terms
of .accept, is

      Promise.resolve = v => ( isPromise(v) ? v : Promise.accept(v) );

This avoids even a transient allocation cost when Promise.resolve is used
to coerce (or auto-lift if you wish) a possible promise into a guaranteed

> However, what does
> > aResolve.resolve do? If it causes its promise to accept, this must be
> > observably different to .flatMap observers than if it causes its promise
> to
> > adopt. This difference is not observable to .then observers, which is why
> > I've accidentally missed this issue twice now. But since an
> implementation
> > cannot know ahead of time whether there might be .flatMap observers,
> using
> > .accept for .resolve would impose prohibitive storage costs on .then
> > oriented patterns. See the message Anne linked to.
> I'm not entirely certain I get your point, so let me restate in code
> and hopefully clearer text.  Given this code:
> p1 = Promise.resolve(v1)
> p2 = Promise.resolve(p1)
> p2.flatMap(print) would print the p1 object, but p2.then(print) would
> print v1.  If p2 was the only thing referring to p1, and we somehow
> knew that you'd only interact with p2 via .then(), we could GC p1 and
> just keep v1 around.  However, since we don't know that, we have to
> keep both p1 and v1 around, which is an extra memory cost.
> Is this what you were referring to?

Exactly! When going around a tail recursive async loop, these unnecessary
p1s pile up. See Q.async at <>
for example.

> (I have no idea why this paragraph I'm responding to draws a
> distinction between Promise.resolve() and the
> PromiseResolver#resolve() method, though.  `Promise.resolve(v1)` is
> exactly identical to `new Promise(r=>r.resolve(v1))` - it's just a
> typing shortcut.)

In that previous message of mine, that distinction was indeed unnecessary.
It was only because I wanted to emphasize the PromiseResolver#resolve case,
as it seemed clearer to me that we can't afford the extra wrapping there.
However, I was wrong.

Nevertheless, the distinction here may or may not be needed depending on
your answer to a question: In

    new Promise(r=>r.resolve(v1))

if v1 is a promise, does this "new" call return v1 itself? Clearly, if the
resolver is only stored during construction and called later, the answer
would be no. The returned promise can only adopt v1 later. But literally in
the code above the resolver's .resolve method is called during
construction, so this seems a sensible option. OTOH, even though it is an
allowed behavior for "new" to not return a fresh object, so I think I
prefer the answer "no, the 'new Promise(...)' expression must return a
fresh promise".

If the answer is yes, then indeed the two expressions mean exactly the same

> >> (Note that if anyone thinks we need something that eagerly flattens a
> >> promise, rather than flattening happening implicitly via the
> >> definition of "fulfilled value" for then(), realize that this eager
> >> flattening operation is hostile to lazy promises.  While this might be
> >> *useful* in some cases, it's probably not something we need or want in
> >> the core language, or if we do, it should be given an appropriately
> >> descriptive name, rather than yet another synonym for "accept".)
> >
> > I agree that we do not need eager flattening.
> I don't understand what you're asking for, then.

Did anything in any of my messages imply that I desire eager flattening?

> ~TJ

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <>

More information about the es-discuss mailing list