Killing `Promise.fulfill`

Mark S. Miller erights at google.com
Tue Aug 20 20:59:25 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:
>
>> On Tue, Aug 20, 2013 at 9:18 AM, Mark S. Miller <erights at google.com>
>> 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
> agreement?
>
>
>
>>
>> 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
> 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.
>
>
>>  (If you were to put q2 directly into another promise, via
>> `p2 = Promise.resolve(q2)`, then p2 would be resolved.
>
>
> yes.
>
>
>
>>  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.
>>
>
> yes.
>
>
>
>>
>> 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"
>>
>
> "unresolved"
>
>
>> 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"? ^_^
>>
>
> "unresolved"
>
>
>>
>> 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
> promise.
>
>
>> 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 <
> http://wiki.ecmascript.org/doku.php?id=strawman:async_functions#reference_implementation>
> 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
>

Should be: "...even though it is an allowed behavior for 'new' to not
return a fresh object, it is surprising, 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.
>
>
>>
>> >> (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
>>
>
>
>
> --
>     Cheers,
>     --MarkM
>



-- 
    Cheers,
    --MarkM
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20130820/540d303f/attachment-0001.html>


More information about the es-discuss mailing list