Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way of synchronously retrieving fulfillment values and rejection reasons #61

Closed
domenic opened this issue Jan 4, 2013 · 23 comments
Closed

Comments

@domenic
Copy link
Member

domenic commented Jan 4, 2013

This is a new idea that does not belong in the base spec; we are just using this issue tracker as a place for discussion.

Over in kriskowal/q#167, we're contemplating how to retrieve the value of a foreign fulfilled or rejected promise. The use case there is having an isPending(possiblyForeignPromise) predicate that doesn't give false positives, but I think the most compelling use case is speed for e.g. templating systems (to avoid DOM manipulation). I believe @ForbesLindesay has mentioned this being used in q-ejs, and I know @wycats had some interest in the issue as well. I've also heard second-hand that @lukehoban wasn't sure promises could be always-asynchronous because of such use cases.

This is largely a bikeshedding matter, but it'd be nice to have an interoperable standard. Currently Q uses the following semantics, which to be honest I'm not a huge fan of:

  • promise.valueOf() on a pending or rejected promise: returns promise.
  • promise.valueOf() on a fulfilled promise: returns the fulfillment value.
  • promise.valueOf().exception on a rejected promise: returns the rejection reason.

Other prior art:

  • RSVP uses resolvedValue and rejectedValue. Problems: uses the horribly-confusing term "resolve," and no way to distinguish between pending vs. fulfilled or rejected with undefined.
  • Mark Miller's concurrency strawman: promise.nearer() is like Q's promise.valueOf()
  • Can't find much more??
@domenic
Copy link
Member Author

domenic commented Jan 4, 2013

My initial idea (not great though, I admit):

  • promise.value is the fulfillment value, or undefined if not present
  • promise.reason is the rejection reason, or undefined if not present
  • promise.isPending is a boolean allowing you to distinguish undefined fulfillment values or rejection reasons from pending promises.

The last bullet point could be replaced by three booleans (isPending, isRejected, isFulfilled) or a single state value (state is one of "pending", "rejected", or "fulfilled").

Alternately the predicates could be on some "promise tester object", which in practice always ends up being the promise library's main entry point, e.g. Q.isPending(promise), Q.isRejected(promise), etc.

@briancavalier
Copy link
Member

I'm not a fan of synchronous value/state extraction, in general, but if there are compelling practical use cases, I can probably be swayed. I just worry about people doing strange things like polling a promise using setInterval or somesuch instead of simply using then.

That said, I prefer promise.nearer() over promise.valueOf, since I think valueOf's intended purpose is to coerce to a primitive type, and that wouldn't always be the case here.

If I understand promise.nearer() correctly (I fully admit that I may not!), it seems like it provides enough functionality for any library to implement isPending and isFulfilled, but it's not clear to me how you'd implement isRejected using only promise.nearer() ... hmmmm.

@domenic
Copy link
Member Author

domenic commented Jan 4, 2013

I'm not a fan of synchronous value/state extraction, in general, but if there are compelling practical use cases, I can probably be swayed.

Agreed. It's the templating use case that really convinced me.

That said, I prefer promise.nearer() over promise.valueOf, since I think valueOf's intended purpose is to coerce to a primitive type, and that wouldn't always be the case here.

+1

If I understand promise.nearer() correctly (I fully admit that I may not!), it seems like it provides enough functionality for any library to implement isPending and isFulfilled, but it's not clear to me how you'd implement isRejected using only promise.nearer() ... hmmmm.

I'm pretty sure this is correct.

@unscriptable
Copy link
Member

@domenic:

Agreed. It's the templating use case that really convinced me.

A link to this would be great for my education since every use case for sync value/state extraction I've seen so far has been due to sync/async interaction which could be handled in a better manner. Thanks!

@domenic
Copy link
Member Author

domenic commented Jan 4, 2013

@wycats want to help @unscriptable out with an example?

@medikoo
Copy link

medikoo commented Jan 4, 2013

I think logically valueOf should return either fulfilled or rejected value of resolved promise (not just fulfilled), and that's what I do in deferred but in deferred also then on resolved promise is synchronous, but that's the other story ;)

@kriskowal
Copy link
Member

I too am not so much of a fan of my Q’s naming conventions in this matter either, but the algorithm is solid. I might be convinced to change the names. I might also be convinced to change mostResolved (née nearer, unwisely experimentally changed to valueOf by @kriskowal) such that it always returns a promise instead of returning a fulfilled value.

  • Deferred promises
    • In a pending state
      • mostResolved() returns this
    • After resolve(value), resolution = coerce(value).
      • mostResolved() returns resolution.mostResolved()
  • Fulfilled promise
    • mostResolved() returns this
    • has a value property with the fulfillment value
  • Rejected promise
    • mostResolved() returns this
    • has an error property with the rejection reason / error / exception
function isPending(object) {
    return !isFulfilled(object) && !isRejected(object);
}

function isFulfilled(object) {
    return !isPromise(object) || "value" in object.mostResolved();
}

function isRejected(object) {
    return isPromise(object) && "error" in object.mostResolved();
}

@briancavalier
Copy link
Member

@kriskowal This looks pretty good to me at first glance. Maybe s/error/reason, but that's very minor. I need to digest it a bit more, tho ... hopefully over the weekend.

@domenic
Copy link
Member Author

domenic commented Jan 4, 2013

@kriskowal wow, that really clarifies the meaning of mostResolved. Definitely like making it always return promises. Except again the use of that "r-word" in its name ;).

I'm also quite happy about the use of "value" in promise, as opposed to the buggy promise.value === undefined, as a means of differentiating. I'm surprised I didn't think of it! Even apart from the mostResolved idea, it solves the essential problem of the OP.

@kriskowal
Copy link
Member

@domenic Replace mostResolved with mostXed where X is the name of whatever function sets the resolution of a deferred promise.

@unscriptable
Copy link
Member

Very, very interesting. I actually like that devs have to do something more involved than .then() to "get at the value synchronously". Hopefully, it makes them think twice. :)

I agree @domenic: returning a promise also helps make it more apparent what's really going on (imho, anyway).

This may be a bit off-topic, but can somebody succinctly describe why "nearer" is a good alternative to "mostResolved" (ignoring the r-word argument).

@kriskowal
Copy link
Member

@unscriptable Only Mark Miller could succinctly explain that. Using promises as proxies for remote objects introduces more kinds of promises, like Near promises and Far promises. A far promise can be fulfilled remotely, resolved locally, but still represented as a promise object.

@briancavalier
Copy link
Member

I did a quick experiment to get my head around this approach in cujojs/when@f929a35. And here is a gist with a simple node test program, and its output. It turned out to be quite simple to implement.

One interesting issue it raised was what to do with a promise-like thenable that doesn't support nearest. I just check for it and return the thenable if it doesn't support nearest ... can't think of anything better, but it means isPending, isFulfilled, and isRejected (obviously) can't work in that case. Documentation may be the only solution there.

For fun, I tried out the name nearest rather than mostResolved or nearer. My thinking was that it's similar to the superlative "most" in "mostResolved", but is a single, lowercase word, and implies that you want the promise nearest to the final value/reason (at least in my head!). Dunno which I like best yet. mostResolved has the "resolved" problem, but is otherwise very descriptive--it'd be nice to find a non-compound word, tho. Ah naming ...

Anyway, I'm still not sold on the need for this, so it'll be great to hear from @wycats on some concrete use cases.

@ForbesLindesay
Copy link
Member

I found it worthwhile when building QEJS (an asynchronous templating library). The issue there was that I had vast arrays of mostly strings, with a few promises for strings, and a few promises for arrays of strings and so on in a big nested structure. Most of the promises in the array would end up being resolve long before I actually asked for their value. Before optimizing the handling of pre-resolved promises it was unbearably slow, but after it was OK. Sadly I didn't do any proper benchmarks, I just left it with the fact that it made the application noticeably snappier.

I was using Q.all to resolve the promises and all the promises were Q promises. I do plan on building a new version of it that doesn't rely on anything specific to an individual promise implementation.

@briancavalier
Copy link
Member

Hmmm, just had the thought that what nearer/nearest/mostXed does, conceptually, is retrieve the "end" of the promise chain. Maybe that's a clue to another potential name. I'd suggest end(), but afaik, it's already in use as a debug/capping method in several libs. It also might be read as a verb ("end this promise now"). Other possibilities: last, final, tail

@ForbesLindesay
Copy link
Member

I quite like tail.

I think naming of it isn't too important though, other than avoiding something really stupid and avoiding conflicting with anything that's used elsewhere. This method (whatever we call it) will only really be intended for internal use by other promise libraries, so we're talking about advanced users (unlike with then).

@kriskowal
Copy link
Member

tail is too generic IMO, and conflicts with my intuition of the meaning of [head, ...tail] coming from pattern-matching languages.

I don’t think there is a way around using the terms resolver, resolve, and mostResolved. Having talked to Mark Miller this weekend, I don’t think we can even get far away from having isResolved as a shorthand for !mostResolved().isPending(). I think we should just throw in with these terms, despite the confusion caused by resolve(pending) not resulting in a "resolved" state.

@ForbesLindesay
Copy link
Member

An alternative would be something along the lines of:

promise
  .asap(function (value) {
    }, function (err) {
    });

Which would discourage people from relying on this method eventually becoming synchronous. It would essentially be equivalent to .then but would remove the 'next-tick' constraint. It's use in general would still need to be discouraged, but in certain specific cases it could provide a useful performance optimization.

@domenic
Copy link
Member Author

domenic commented Feb 5, 2013

I've created a new repo for this:

https://github.com/promises-aplus/synchronous-inspection-spec

Mainly because we need something like this to fix Q's current implementation.

@domenic domenic closed this as completed Feb 5, 2013
@briancavalier
Copy link
Member

@domenic Can you elaborate a bit on that last comment? What needs fixing? I thought that Q implements something along the lines of the mostResolved algorithm that @kriskowal described above. If I had to pick an approach to sync inspection right now, that would probably be it. Have you found problems with it?

@domenic
Copy link
Member Author

domenic commented Feb 5, 2013

@briancavalier Q has a bug wherein synchronously inspecting a promise rejected with a falsy value does not show it to be rejected, so we just need to fix that. Thus some of the schemes shown here make sense as a fix, since they correctly distinguish.

mostResolved seems to me to be a separate issue from synchronous inspection, and I apologize for conflating the two in my original post. It allows you to get a promise value that is "nearer" to resolution, i.e. if a pending promise is waiting on a given promise, it gives you that promise. That's separate from finding the fulfillment value or rejection reason of a resolved promise.

@kriskowal
Copy link
Member

@domenic, mostResolved or nearer is most definitely coupled since it is necessary to extract the fulfillment value of a deferred promise that has been resolved to a fulfilled promise (and many other similar cases).

@domenic
Copy link
Member Author

domenic commented Feb 5, 2013

@kriskowal why is it necessary? It's necessary in Q currently, but why is it inherently necessary? Such a promise still has a fulfillment value and rejection reason which it would be fine to expose separately from mostResolved/nearer functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants