-
-
Notifications
You must be signed in to change notification settings - Fork 146
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
Implement AsyncInterop\Promise #78
Conversation
@jsor I fixed an issue with the |
@trowski Thanks 👍 |
FYI: this effects this issue async-interop/event-loop#128 |
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.
Aside from my earlier comment about the possible namespace change
@WyriHaximus Upgraded to async-interop/promise ^0.4.0 with the new namespace. |
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.
As much as I welcome interoperability with others, I'll have to vote 👎 on this for now.
My main concern here is how we now also offer a when()
method while we currently focus on the then()
method. If we were to drop our then()
method this would make sense, but I don't really see this happen any time soon.
From a consumer perspective this is rather confusing and/or surprising, I wonder which method I should chose in which situation. At the very least, this would require some elaborate documentation and/or examples. The documentation currently links to and describes Promise/A+, but not the concept of having a when()
method.
I have to admit my main concern should probably be directed at async-interop/promise and not this PR. I'll look into this in the upcoming days.
This reverts commit 4b42c60.
@clue I don't see this as an issue. I intentionally left out any documention of
The benefit is, that |
My main concern with not extending the interop promise that we can't communicate both interfaces that way. While all promise classes implement both interfaces, there isn't a single interface that communicates both interfaces. This is mainly coming from my work on @php-api-clients with PHP 7 return types. For example the following function has a react promise return type hint as it can be a function r(): ReactPromiseInterface Now the following function only accepts a function g(AsyncInteropPromise $promise): AsyncInteropPromise Technically Now this all assumes PHP 7 (which I hope we'll move more towards this year (maybe even PHP 7 only release, but that's a different discussion)) return types and type hints. Having a single type you can hint makes it cleaner for end users IMHO compated to docblocks, or even IDE detection of possible return values. From the work I've been doing over at @php-api-clients perspective depending on how we do the interop matters in user experience. There are two options, extending would mean I can return While with implementing both I agree with @jsor that Just my two euro's. |
I agree with everything @WyriHaximus mentioned above. React's promises should extend the async-interop interface to make usability easier for everyone. Linking to the async-interop standard should be sufficient to explain the inclusion of |
I disagree for the reason @clue is mentioning: confusion for the consumer. My proposal here is, that the interop API is completely hidden from the consumer. They should just not use it. It is only intended for other libraries making it easier to consume react's promises through their Regarding @WyriHaximus example: function g(AsyncInteropPromise $promise): AsyncInteropPromise I think that this is just wrong. Why typehint against the placeholder rather the actual value the function is working on? IMHO, the correct signature is function g(MyValue $value): AsyncInteropPromise And the function should be called like React\Promise\resolve($promise)->done(function (MyValue $myValue) {
g($myValue);
});
// or in a couroutine based library like amphp
/** @var $myValue MyValue **/
$myValue = yield $promise;
g($myValue); |
The issue with React's promises not extending the async-interop interface is functions declaring |
I think @jsor is absolutely right there. But only as long as it's not some kind of combinator function. Combinator functions also benefit from interoperable promises, alongside with coroutines. If If all of your own implementations implement both interfaces, it doesn't matter to them whether they do that or implement only The only difference is that custom implementations of For the consumer, nothing changes. All it changes is - as said - custom implementations. If you want to hide the |
I think this is a good thing because it prevents from mixing up async-interop promises with higher level placeholder abstractions like JS-style promises, coroutines, Rx obervables etc.. The async-interop interface just let you convert between these concepts.
I think that's exactly not the purpose of the async-interop interface. Choose the promise/coroutine/rx/whatever-style library you prefer. If you use a dependency which prefers another style, the async-interop interface enables to convert from one style to another. |
That is exactly the aim of the interop spec. Being able to consume whatever implementation is used. Not adapting everything. If we want to adapt things, we don't need an interop spec, we can just write adapters for the most common libraries. |
Right, that's what the interop spec is for. You can choose whatever you prefer, but you can always consume it via the interface defined in the interop spec. But that's only possible if Quoting from the current specification:
|
You could, but why would you? Why would you use
Why that? |
To be able to pass it directly into generic functionality … Coroutines, combinators etc. They all use the standards sole when() function. |
Amp doesn't provide any sugar on it's promise implementation. The sugar are coroutines and combinator functions, they directly consume any promise implementation using So the application developer usually doesn't even call
Because otherwise not every |
There is no doubt that @jsor is right. That code snippet was merely to illustrate my point that when When |
My point is, that there's no such thing as "generic" :)
I'm not completely against extending the async-interop interface but this might need some discussion with @reactphp/core. |
@jsor We tried repeatedly to get input from you guys, but nothing happened apart from @WyriHaximus participating and the some activity from you on the latest PRs. If you guys do not extend the interface now, then the promise interop project has failed.
Currently, because there's no reason to have
The goal is enabling that to all promise implementations. So users might continue to use
Nobody suggests to drop
Please do so. |
As a ReactPHP user, I'd love to see the this proposal implemented. |
|
||
If you're using React/Promise as your primary value placeholder implementation, | ||
it is recommended to **not** use the `when()` method directly but the methods | ||
provided by the [`PromiseInterface`](#promiseinterface). |
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 think this can be reworded to: "If you use react/promise
implementations, it's recommended to use the high level consumption methods then()
, done()
, otherwise()
and always()
to consume a promise's value or 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.
From a consumer perspective, I actually prefer the explicit recommendation against when()
because this would otherwise leave me wondering why it exists in the first place. However, I also like your suggestion of explicitly pointing the user to the correct alternatives 👍
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.
Well, doesn't the explicit recommendation against it make you wonder even more why it exists? The alternatives are in no way more "correct" than when()
, they're just more high-level, and that's how I'd personally word it. You can totally use when()
, but be aware of it being low-level. I think it makes sense to mention a reason when to use when()
: It makes sense whenever your function accepts promises instead of direct values, as it can accept any promise then from any vendor.
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.
Sorry for my wording, "correct" wasn't intended to mean the alternative is "incorrect", but rather "not recommended and/or endorsed".
You can totally use when(), but be aware of it being low-level.
I guess this is the culprit here: Why would react/promise endorse a low-level API to its consumers if it offers a (conceived or otherwise) more powerful and comfortable higher-level API? See also the comments below 👍
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 totally depends on what's consumer for you. Is it just application developers or also library developers? The latter will probably want to use when()
if they also accept other promise implementations instead of wrapping all promises in React's promises. If all you need is a simple callback on completion (no chains), when()
is pretty equivalent to always()
/ done()
(depending whether you need the value / reason or not), except that it works on all compatible implementations then, not only React.
In fact, you might want to adapt done()
to when()
semantics regarding the error handling to provide a clear separation of concerns. IMO, throwing from resolve
/ reject
because the consumption of the value failed is wrong. @jsor mentioned that in our face2face meeting as one of the ugliest things in the current promise implementation.
<?php
require __DIR__ . "/vendor/autoload.php";
$deferred = new React\Promise\Deferred;
$deferred->promise()->done(function ($value) {
var_dump("value", $value);
}, function ($reason) {
throw new Exception;
var_dump("reason", $reason);
});
try {
// shouldn't throw here, instead forward to the interop error handler
$deferred->reject("foobar");
} catch (Exception $e) {
var_dump("catch", $e);
}
Sorry for the lengthy discussion here, should I open a separate issue for that one? It depends on this PR, but I think it could make sense for 3.0.
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.
@jsor You're right about always
, thought it would only allow clean ups, missed that it supports forwarding as well.
My main point is only about this statement from your README:
Calling done() transfers all responsibility for errors to your code. If an error (either a thrown exception or returned rejection) escapes the $onFulfilled or $onRejected callbacks you provide to done, it will be rethrown in an uncatchable way causing a fatal error.
Because in fact, those errors aren't uncatchable, they bubble up to $deferred->resolve()
and then further up from there. To prevent exactly the bubbling up to $deferred->resolve()
, the interop spec introduced the ErrorHandler
which can either log such an error gracefully or abort the script if not such handler is defined.
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.
Because in fact, those errors aren't uncatchable, they bubble up to $deferred->resolve() and then further up from there.
Yes, they are catchable if resolution happens synchronously. catchable is a bit misleading here as it means uncatchable by any handler, not (only) from try/catch.
the interop spec introduced the ErrorHandler which can either log such an error gracefully or abort the script if not such handler is defined.
Nothing prevents you from transferring the error to the ErrorHandler from inside done().
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.
This has really become quite lengthy now. Want to continue in chat?
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 might hop in when time allows. But, we're not going to change the behavior of done()
. It's a defacto standard among promise libraries and it's is in line with all implementations i'm aware of.
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.
@jsor Bluebird for one doesn't let errors from done
bubble up to the resolve
call, it executes async.fatalError
and thus exists. If it's not in node, it doesn't let it bubble up either, it defers it to the next tick and throws from a 0
timeout callback, similar what we had before we had the error handler. But as we don't have the a loop in the language, we wanted to decouple those and introduced a separate error handler.
method. | ||
|
||
The `AsyncInterop\Promise` interface is intended for interoperability with other | ||
libraries and their combinator and conversion functions. |
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.
Suggestion: "The AsyncInterop\Promise
interface allows interoperability with other promise implementations and their combinator and coroutine implementations."
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.
@kelunik While I understand your point, I actually prefer the current version (which is slightly more conservative and less enthusiastic). After all, while we all aim for this, "allows interoperability" has yet to be proven :-)
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.
@jsor Can we add a very simple example to show how this interoperability may look like from a consumer perspective? Such as a dummy combinator function with the proper typehint etc.?
See also above for the documentation and/or example on how we can consume an AsyncInteropPromise.
libraries and their combinator and conversion functions. | ||
|
||
Note, that although the interface is named *Promise*, it has nothing to do with | ||
promises commonly known from the |
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'd reword this paragraph. While the interop promise may have nothing to do with the concrete interface of A+, it's still the same thing. A placeholder representing the result of an async operation.
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.
@kelunik While I understand your point, I actually prefer the current version from a consumer perspective who is familiar with the term "promise" from the other projects enlisted below. I like the "A placeholder representing the result of an async operation." though 👍
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.
@clue I understand your point, but I'm not sure whether it really helps consumers to understand it. They'll just think "Why the hell is it called promise if it's something entirely different?!" and be more confused than before. We had a really long discussion about the naming in async-interop/promise#4 and another one to switch back to Promise
, but I can't find that one right now. The reason behind it is that we kept calling it Promise
while talking about it, because it's just that, just with a different consumer API.
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.
"Why the hell is it called promise if it's something entirely different?!"
I think this is a very important point here: To me, this whole PR seems to convey the notion that this library implements a concept commonly known as "promises", while async-interop/promise only offer a low-level API that aims to ease interoperability between different promise vendors.
We had a really long discussion about the naming in async-interop/promise#4 and another one to switch back to Promise, but I can't find that one right now.
This isn't exactly transparent to me either, so I'd very much appreciate if you happen to find this and could link to this here 👍
[…] we kept calling it Promise while talking about it, because it's just that, just with a different consumer API.
I understand the reasoning – but would disagree from a consumer perspective. If there's project A (e.g. this project) that describes "promises" and the interop standard that describes a different API then it's not the same from a consumer perspective.
This means that project A should either ditch its description and adopt the interop API or the other way around (which I don't see happening any time soon).
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.
@clue @kelunik Here is the related PR changing the name back to Promise: async-interop/promise#15
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.
Thanks all for the discussion in this thread so far, I think this actually helped form and improve the current suggestion 👍
Also thanks @jsor, I think the current version with its documentation makes much more sense from a consumer perspective 👍
method. | ||
|
||
The `AsyncInterop\Promise` interface is intended for interoperability with other | ||
libraries and their combinator and conversion functions. |
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.
@kelunik While I understand your point, I actually prefer the current version (which is slightly more conservative and less enthusiastic). After all, while we all aim for this, "allows interoperability" has yet to be proven :-)
method. | ||
|
||
The `AsyncInterop\Promise` interface is intended for interoperability with other | ||
libraries and their combinator and conversion functions. |
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.
@jsor Can we add a very simple example to show how this interoperability may look like from a consumer perspective? Such as a dummy combinator function with the proper typehint etc.?
See also above for the documentation and/or example on how we can consume an AsyncInteropPromise.
libraries and their combinator and conversion functions. | ||
|
||
Note, that although the interface is named *Promise*, it has nothing to do with | ||
promises commonly known from the |
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.
@kelunik While I understand your point, I actually prefer the current version from a consumer perspective who is familiar with the term "promise" from the other projects enlisted below. I like the "A placeholder representing the result of an async operation." though 👍
|
||
If you're using React/Promise as your primary value placeholder implementation, | ||
it is recommended to **not** use the `when()` method directly but the methods | ||
provided by the [`PromiseInterface`](#promiseinterface). |
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.
From a consumer perspective, I actually prefer the explicit recommendation against when()
because this would otherwise leave me wondering why it exists in the first place. However, I also like your suggestion of explicitly pointing the user to the correct alternatives 👍
@@ -402,6 +408,10 @@ Creates a promise for the supplied `$promiseOrValue`. | |||
If `$promiseOrValue` is a value, it will be the resolution value of the | |||
returned promise. | |||
|
|||
If `$promiseOrValue` is a | |||
[`AsyncInterop\Promise`](https://github.com/async-interop/promise), a trusted | |||
promise that follows the state of the `AsyncInterop\Promise` is returned. |
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.
Can we add a very simple example how we can consume an AsyncInteropPromise? Such as passing this through the resolve()
function etc.?
@clue What's the current progress? Is this PR ready to be merged? |
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.
Thanks @jsor, your suggested changes LGTM and there's currently nothing I'd ask you to change 👍 However, I'm not comfortable approving this yet and will leave my vote unchanged (for now) for the following reasons:
To me, this whole PR seems to convey the notion that this library implements a concept commonly known as "promises", while async-interop/promise only offer a low-level API that aims to ease interoperability between different promise vendors.
I guess this is the culprit here: Why would react/promise endorse a low-level API to its consumers if it offers a (conceived or otherwise) more powerful and comfortable higher-level API?
I understand my main concern should probably be directed at AsyncInterop instead, so I'm not sure it even makes sense to address this here.
This PR has received some interest among the people behind AsyncInterop, so perhaps you can also shed some light on this: What do you think about this PR in its current state?
Because it makes React compatible with coroutine implementations and generic combinator functions. |
@clue By implementing When an API returns a React promise, a user should be encouraged to use the high-level API of the React promise. However, that same user can also know that they may use that promise in conjunction with any other library that accepts interop promises.
Documentation of Hopefully this will help you understand the purpose of this PR and the async-interop group. It is not to replace the API of any particular group, but rather allow sharing of code between libraries of different groups. We want to push async in PHP forward, making it as easy as possible to share code and collaborate, allowing each group to code to their own style and preferences, without having to worry about compatibility with a particular event loop or promise implementation. |
Closing this for now. It might be reconsidered at a later point. |
Interoperability with async-interop/promise.
Closes #64