-
Notifications
You must be signed in to change notification settings - Fork 164
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
Add the "Thenable Assimilation Procedure". Closes #75. #76
Conversation
1. If `x` is not a thenable, `promise` must be fulfilled with `x`. | ||
1. If `x` is a thenable, run `Assimilate(promise, x)`. | ||
1. If/when `rejectPromise` is called with a reason `reason`, `promise` must be rejected with `reason`. | ||
1. If both `resolvePromise` and `rejectPromise` are called, or multiple calls to the same argument are made, the first call takes precendence, and any further calls are ignored. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this conflicts with "If x
is a thenable, run Assimilate(promise, x)
", since that'll lead to resolvePromise
being called multiple times. Perhaps it should be clarified that a) resolvePromise
is whatever means of resolving the promise who's trying to assimilate thenable
's state, and b) this restriction only applies to the current assimilation.
Note that without b), a broken thenable
could do the following:
var thenable = {
then: function(resolve){
resolve(otherThenable);
resolve(true);
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, this doesn't seem unclear to me. resolvePromise
and rejectPromise
are "local" to the current Assimilate(promise, thenable)
call, and I think this is communicated somewhat clearly already...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upon re-reading, thenable.then(resolvePromise, rejectPromise)
does indeed imply that resolvePromise
is "whatever means of resolving the promise", but it could also be taken to mean a resolve
method from the promise's resolver.
The distinction is important, because it means you can't simply pass resolve
to the thenable as that may lead to the scenario outlined above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, we don't define resolvers in this spec; the entire behavior of the resolvePromise
function is defined a few lines above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was fairly clear to me, although it may not hurt to provide a very short description of what resolvePromise
and rejectPromise
are/do. May not be necessary, tho.
Implementation from line 68 in https://github.com/novemberborn/legendary/blob/master/lib/Promise.js#L68. |
1. If both `resolvePromise` and `rejectPromise` are called, or multiple calls to the same argument are made, the first call takes precendence, and any further calls are ignored. | ||
1. If the call to `thenable.then` throws an exception, | ||
1. If `resolvePromise` or `rejectPromise` have been called, ignore it. | ||
1. Otherwise, `promise` must be rejected with the thrown exception as the reason. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to suggest here that we should be explicit here about calling rejectPromise(exception)
, but actually, I like it the way it is. This current language allows the implementation to use any implementation-specific means to reject promise
, which is nice.
s/Assimilation/Adoption?? |
"Transmogrification" ftw ... j/k I use "assimilation" myself, so I prefer it (mostly out of habit?). It has a stronger implication of "become like" in my mind, but "adoption" can be similar, e.g. "adopting local customs". |
Slightly updated. Would like to merge by this weekend and push out the 1.1 release; let me know what you think. |
Would especially appreciate @lsmith's wordsmithing :) |
I don't quite follow the difference between how a promise and a thenable are assimilated. Are we assuming promises will have a synchronous way of getting its result or other ways of listening to its state change other than |
Another point. Although I don't think we need to standardize "resolvers", the spec would be a lot easier to read and parse if we added "Resolve", "Fulfill" and "Reject" as algorithms/functions. That way instead of writing 600 times stuff like "If I'll try to write something in the style of the EcmaScript spec. |
I'll take a look at this tonight. Sorry I haven't been able to get to it until now. |
Yes, exactly that. An implementation-specific way, if nothing else. Did you see the footnote? |
I see where you're coming from, and somewhat agree. But, my gut feeling is that this would hurt the simplicity of the current spec, which is why I didn't try it. Even the Thenable Assimilation Procedure section seems a bit out of place in the current spec, which has this nice explanation of the Happy to be proved wrong though! |
Are the following inconsistent?
vs.
It seems like if an implementation allows promises-for-thenables, they are. Implementations should not allow such things, but that's outside the scope of the spec. Maybe it should be revised to
|
@domenic Wouldn't "resolved" instead of "fulfilled" make much more sense? Or they become synonyms in 1.1? I'm a little confused here. |
@rkatic indeed, they are synonyms when |
I see. However I would be more in favor for consistency, or at least not having contradictions between specs. Am I wrong on seeing any? |
What is the contradiction you are seeing? Maybe quoting a specific line would be more helpful. |
I am referring to:
I would expect "resolved" here, even if there are no clear distinctions between those two words in this single spec. |
What other line of this spec does that line contradict? |
I am not referring to contradictions inside this spec alone, but eventually between this spec and the resolver-spec. |
In that case, what line of the resolver spec does that line contradict? |
If I am right, according to the terminology in promises-aplus/constructor-spec#18 (first section), "fulfilled" is an already "Settled" promise, therefore the "Resolved" term should be used here. |
We don't want to fulfill |
I am aware of that (or missing something) and still consider "fulfilled" kinda wrong here. Thanks for trying anyway. |
This seems to limit the extent to which you can assimilate a promise. Specifically I think this might be worded in such a way as to prohibit progress propagation and cancellation propagation. It might just be my reading of it though. Perhaps the wording needs to be clearer? |
@ForbesLindesay could you outline the problem in more detail? In general, I think it is problematic, since Q at least has the ability to mark promises as "don't try to resolve me" via Also, I think this is important and hope we can figure out some good wording and push it out soon, since I really want to get it in to 1.1. I don't think it's feasible to release 1.1 with only the fixes we have, only to introduce a change like this shortly afterward. I'd like 1.1 to be the only real revision we end up doing. But it's blocked on this :-/. |
We could add a footnote to the definition of thenable, that allows exceptions for objects implementations have marked as "not a promise" via some specific mechanism. Seems OK. That just solves the problem I was talking about, not @ForbesLindesay's, though. |
When I call: promise.then(function () {
return foreignPromiseThatEmitsProgress;
}).then(null, null, function (prog) {
console.log(prog);
}); I should see progress being logged, ideally. If assimilate is just a simple implementation of the algorithm described, that won't happen. The implication of the spec is that doing anything else in addition to what's described isn't allowed, and so I couldn't extend the algorithm to also propagate progress. The same argument follows for cancellation. |
P.S. I'd quite like this spec to reserve the third argument for progress use in this spec at some point. |
The marking of thenables as "not a promise" is unpractical since different implementations would use different "markers". |
I really hate the idea of supporting "thenables that aren't close enough to promises to be assimilated" - I've never seen one. If you do see one, handle it by explicitly converting it to something that's not "thenable" before it can be assimilated. |
To help make clear how much of a non-issue I think thenables that need to be passed through should be for this core spec I've created an extremely robust wrap/unwrap solution called |
@ForbesLindesay At certain extent, I understand your "hate", but not allowing thenable fulfilled values would make Promise/A+ incompatible with other promise specs that allows that. Using a wrapper would certainly complicate usage of a Promise/A+ implementation with others. |
Would it really be so bad to also have a |
You have 3 options:
Marking thenables is fine with me, except that it would be difficult to ensure interoperability (we'd have to agree a marking strategy) Marking promises is a non-starter, there are too many promise implementations and part of the point is to do our best to assimilate promises that weren't necessarily designed to be perfect promises/A+ promises, they're just moderately well behaved thenables. Wrapping promises is fine, it has no overhead for anyone except those people who have decided to use thenables that aren't promises along with a promise library (I suspect that may be the empty set unless you're in it). The compatibility issue with other specs you suggest is a non-issue since we're already incompatible with http://wiki.commonjs.org/wiki/Promises/A which ought to be the easiest one to be compatible with. What we need is compatibility with what people commonly do, and I think this provides that. As for In short, creating promises for thenables is incompatible with the goal of making it impossible (or at least very difficult) to create promises for promises. As a final point, you could call |
Not sure what you mean here. Could you show a simple example? If you start with the goal that there should not be promises for promises, then of course that a |
The case of
function fulfill(val) {
return {then: function (onFulfilled) { onFulfilled(val); } };
}
promise
.then(function () {
return fulfill(fulfill(fulfill('foo')))
}).then(function (val) {
assert(val === 'foo');
}); assimilate just gets called 3 times until it's finished unwrapping the promise, then it's done. The behavior you want is:
That would let you have: function fulfill(val) {
return {then: function (onFulfilled) { onFulfilled(val); } };
}
var sentinel = fulfill(fulfill('foo'));
promise
.then(function () {
return fulfill(sentinel )
}).then(function (val) {
assert(val === sentinel);
}); The fulfill method is trivial, nobody's stopping you creating a promise for a promise, and nowhere in the spec prohibits it. What the spec does prohibit is getting one of your promises for a promise assimilated into someone else's library. |
I think you have illustrated correctly what I was talking about, beside that
Why is this important? |
Because promises for promises are generally considered bad. This is a fact that has been discussed at length in many other issues. I'm fine with bad things happening in your library, as long as I have a method to ensure that they don't cross the boundary into my library. If the spec prohibits me from preventing your bad stuff from entering my library then I have a problem. As for why promises for promises are bad: They make debugging harder by adding an un-necessary layer of nesting. They have a tendency to "escape" into areas of an API that weren't supposed to have promises in them. For example It also breaks the say what you mean rule:
A promise for a promise represents that second promise, which is already available, because promises always are (you can create a new promise that can act as a proxy, so is equivalent). As such, you're not saying what you mean.
That by contrast does say what you mean. |
This is not exactly a problem, in my opinion, because my fulfilled value is not intended to be used as a promise any more. The real problem is when such areas, at some point, do support promises, and the value is used as a promise again. Even if this is not a problem with promises that fulfills with itself (if assimilation would work in my way), it still remains a problem in other cases. A perhaps even bigger problem, is thinking to "sanitize" a thenable by recursively resolving it. Not only it would stuck on resolving a cyclic thenable, but also it would try to use a fulfilled value as a promise, even if it is not intended to be used as a promise any more. I'm not sure that wrapping promises is a valid solution. Once the promise is unwrapped for usage, it could "escape" again. Keeping something "constantly" wrapped could be a real challenge or a too big pain to do. At this moment, I think the less dangerous solution is to make user responsible for not making such thenable values "escape", instead of giving a wrong sense of security. |
I am aware that I am new here, and that because of many already conducted discussions, a huge (?) majority here have already took an (almost) clear position on how things should work in this spec. |
I think you're correct. As far as I'm aware it's an API that's rarely (if ever) used to actually create promises for "thenables which aren't promises though" so it would just be a case of making the change and jumping in to help firefight issues people have as they arise (there would be a few scattered here and there). If we can get buy in from key players (especially @kriskowal as Q is the only library that I know does this) then I don't see any problem with moving ahead down that path, the focus now needs to be getting consensus among implementers. |
Unfortunately, it seems that it is used (or considered useful), and that it's the reason of not removing |
I haven't been able to keep up with this thread as closely lately, sorry guys. @rkatic and @ForbesLindesay: It's not clear to me what's being proposed in these few messages relating to Q.fulfill and fulfilling promises with thenables. Could one of you summarize what you're proposing? Thanks! |
@briancavalier You didn't miss anything important I guess. |
No worries, @rkatic. Thanks for summarizing. |
I realize that this thread has gotten a bit sidetracked in places, but I've seen no outstanding issues, and think it's time to move forward and publish a 1.1 with this change. With that said, I'll merge this change, and start a new issue asking for sign-offs from implementers and other involved parties before we officially publish a 1.1, i.e. update the gh-pages branch. |
This more fully specifies the manner in which thenables must be assimilated, which is useful both for clarity and for usage in future specifications, e.g. specifying the behavior of `resolve` in a promise-creation spec. The recursive nature of the thenable assimilation is a change from the current specification; see #75 for more details.
I gave another look to the change, and it seems I forgot something during discussions above. [1] This change allows Promises fulfilled with anything (values propagated as they are). Conclusion: Every Implementation can have promises for (fake) promises [1], but it is not allowed to tollerate those from other implementations [2]. -> Coercing broken by spec. Please tell me that I am missing something. |
@rkatic that is by design. Good implementations will not allow promises-for-thenables, but most importantly, as @ForbesLindesay has explained to you already, at the boundary we cannot allow thenables-for-thenables from other libraries to infiltrate well-behaved libraries. That is, implementations are not allowed to tolerate thenables-for-thenables; chains must be squashed at the boundary. |
Ok, but isn't the "squashed at the boundary" strategy potentially dangerous if the fulfilled value is thenable but does not behave as a promise? Is this chosen because Assimilations should never throw? In that case, I would probably prefer rejecting the promise...maybe... |
If you are using promises with thenables that don't behave as promises, the answer is "don't do that." Unfortunately there's not a better answer (see a good summary here). In particular saying "you should hide your bad-thenables as the fulfillment value of fulfilled promises" is a much worse answer than "you should hide your bad thenables inside a non-promise-related wrapper object." |
Is the "don't do that." thing specified somewhere in the spec? If not, maybe it should... |
This more fully specifies the manner in which thenables must be assimilated, which is useful both for clarity and for usage in future specifications, e.g. specifying the behavior of
resolve
in a promise-creation spec.The recursive nature of the thenable assimilation is a change from the current specification; see #75 for more details.