Killing `Promise.fulfill`

Tab Atkins Jr. jackalmage at gmail.com
Wed Aug 21 13:24:35 PDT 2013


On Tue, Aug 20, 2013 at 8:55 PM, Mark S. Miller <erights at google.com> wrote:
> On Tue, Aug 20, 2013 at 11:04 AM, Tab Atkins Jr. <jackalmage at gmail.com>
> wrote:
>> 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.

Well, one is lower-level than the other, so I think it makes sense to
think of it my way. ^_^

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

Ignoring the storage issue, the point of this thread is that the
current accept/resolve semantics are *indistinguishable* for .then(),
and unnecessary/confusing for .flatMap().

It's fine to use "resolve" in the A+ sense even if the technical
definition is just "puts an arbitrary value in the promise", because
you can't tell the difference from .then().

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

Ah, this is why I thought you wanted a deep flattening operation -
adoption semantics aren't quite what Promises/A+ uses.

I guess I'm okay with resolve() adopting promises and just fulfilling
with other values, if we still have an op which solely puts a value
into the promise.

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

Yes, the mechanics of what happens are clear and agreed-on.  But that
doesn't mean that the action matches a useful state.  I think the only
useful things to call "states" are the observable ones where a
particular callback is called.  Any other states we might invent may
be useful for talking about things internally, but that's of lesser
importance.

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

"fulfilled or rejected" is different from "contains a value" in our
context, because a promise can contain a pending promise.  This will
still call .flatMap() callbacks.  Promises/A+'s use of "settled" to
mean "fulfilled or rejected" is fine with me.

>> 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 semantics.
>
> 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
> fits:
>
>       p2 is accepted only when q2 becomes accepted due to adoption
> semantics.
>
> I can guess which rewrite better fits what you're trying to say, but I'll
> let you clarify.

Yes, your latter implication is precisely what Domenic started this
thread with, and what I've been talking about so far in this thread.

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

Yeah, if "pending" means "not fulfilled", which is the Promises/A+
meaning, then this is true.  I'm fine with that.

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

I don't think we want Promise.resolve() to only sometimes return a new promise.

>> 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
> <http://wiki.ecmascript.org/doku.php?id=strawman:async_functions#reference_implementation>
> for example.

Yeah, makes sense.

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

I meant it in the strict sense - that's literally a desugaring of
Promise.resolve().

~TJ


More information about the es-discuss mailing list