-
-
Notifications
You must be signed in to change notification settings - Fork 24
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
RFC: what to do about promise rejection #16
Comments
Suggestion 1Have a specialisation of repromise which allows ergonomic treatment of a result type. This will mitigate the downsides of eliminating rejection from the core construct. This would basically be an eager maybe monad. Suggestion 2A single function which can handle both resolutions and rejections to handle exceptions in the termination of the pipeline: This is analogous to how observables work |
Cc @jaredly |
@ncthbrt @aantron |
@jaredly I was originally thinking that. But often a side effect will return a value instead of unit. Having the |
@nthcbrt Yes to suggestion 1. I just hope it doesn't bloat the size of the implementation, but I suspect it's going to be ok (current compiled I'm not sure yet how suggestion 2 would play out. Would we make the rejection type always |
@aantron: Yes making the rejection type |
So, if I understand correctly: on a type level, the API would still not look like it supports rejection for the most part. Exceptions would be (a bit quietly) turned into JS rejections, and could be extracted later using |
@ncthbrt I would much rather require more usage of |
@jaredly Fair point. |
After dealing with That being said - When I finally got my head wrapped around promises in JS, I've always viewed them as JS's version of Is there a way that the |
Totally agree. I think the ideal type signature would be: Repromise.t([> `Error('e) | `Ok('a) ]) No rejection madness, but still have a way when wrapping a JS lib's promise to catch exceptions (a.la https://github.com/wokalski/vow) and then at the end you would get the `Error('e) in reason land. |
@phated What do you mean by represented? I'm assuming not implementation details, but something visible from the user's point of view? I can see how @johnhaley81 People would be free to use that type signature with |
I would prefer to see |
@danny-andrews These are separate labeled arguments, not a tuple, which is as you seem to correctly suspect :) |
Inspired by Jane Street's Error module, I standardized on |
Thanks! |
Just wanted to share a story from the Scala community about moving from a design similar to the one suggested here to a bifunctor design. http://degoes.net/articles/effects-without-transformers tl;dr: working with nested monads can be unruly. Monad transformers make it nicer, but have significant performance considerations. Not sure how applicable it is to ReasonML (I don't know enough about to runtime to say), but thought it might be interesting reading in the problem domain nonetheless. |
A few people in this thread have expressed frustrations with dealing with Promise rejections in JS.
Can you elaborate on what you mean by this? I personally hate the fact that errors can be swallowed simply by forgetting a It seems like 90% of use-cases for promises will have failure conditions (most async actions are I/O and can fail in some way) so it seems to make sense to encode that into the construct in some way. |
@danny-andrews a promise doesn't need to reject to force you to handle errors. https://github.com/RationalJS/future and https://github.com/wokalski/vow handle this by making the interface Result-ish (the sam patterns can be achieved with Repromise but it doesn't have the utility methods). My ideal interface (and the one I pretty much described above) is actually the same as |
hey @phated, by 'Result-ish', do you mean result-helpers as fetch("...") /* Repromise.t(result('a, 'e)) */
|> Repromise.mapOk(({body, _}) => Ok(body))
|> Repromise.wait(log)
|> Repromise.mapError(...) I'm currently experimenting a bit with Repromise on the native-side and would like errors to "return early" and skip the success-pipe. If that makes sense. |
We should add Result helpers. |
Ok, I just added They look like this: /* Results. */
let andThenOk:
('a => promise(result('b, 'e)), promise(result('a, 'e))) =>
promise(result('b, 'e));
let andThenError:
('e => promise(result('a, 'e2)), promise(result('a, 'e))) =>
promise(result('a, 'e2));
let mapOk:
('a => 'b, promise(result('a, 'e))) => promise(result('b, 'e));
let mapError:
('e => 'e2, promise(result('a, 'e))) => promise(result('a, 'e2));
let waitOk:
('a => unit, promise(result('a, _))) => unit;
let waitError:
('e => unit, promise(result(_, 'e))) => unit;
/* Options. */
let andThenSome:
('a => promise(option('b)), promise(option('a))) => promise(option('b));
let mapSome:
('a => 'b, promise(option('a))) => promise(option('b));
let waitSome:
('a => unit, promise(option('a))) => unit; I think with the way I coded the Next, I'll add some tests next, and get rid of the dep on I'll maybe also alias |
The bundle size is now ~4K. I can get it down to 3.5K by deleting support for Option. On reflection, Option is probably much less useful than Result, because async errors will almost always carry a payload. However, I don't know if losing Option from the API is really worth 500 bytes of savings. The reason Option is 500 bytes is because BuckleScript has a fancy representation for options, trying gto map I imagine most Reason projects are using Option anyway, though, and will have the dep no matter what. |
Repromise is a proposed Reason binding to JavaScript promises.
Adding types to promises raises some new design questions, rejection being especially tricky. TL;DR: the current proposal is not to allow rejection in the main API at all.
I'm posting this issue to explain why that is, and to offer some notes about other approaches considered. Everything is open to experimentation and redesign, so please discuss :)
Feel free to skip around and read only some parts. The first two sections are important because they describe the problem and the main design considerations. Everything after that is a dump of attempted solutions and other thoughts.
The current main Repromise API looks like this:
There is a separate API,
Repromise.Rejectable
, that does have rejection, and providescatch
for converting to "normal," non-rejectable repromises. This separate API is meant to be used mainly for writing the internals of bindings to JS libraries.Background: JS interop
There are two parts to how Repromise interops with JS:
Passing repromises to JS is easy: every repromise is actually implemented as a JS promise, so you can declare bindings like
For receiving promises from JS, if the JS API provides a promise that the API never rejects, that promise can also be typed directly as a normal repromise:
The remaining, tricky case is when the JS API provides a promise that can be rejected. This is what
Repromise.Rejectable
is for:Usage:
Actually, the normal
Repromise
andRepromise.Rejectable
have exactly the same implementation:The types of all the functions, and that there is no way to construct
never
, mean thatRepromise.Rejectable.catch
must be called to convert rejectable repromises into normal ones. This is meant to encourage(/force? :p) bindings authors to offer any error-handling strategy other than native JS rejection: the example above suggestsresult
. The reason for that is explained in the next section.I am not sure how useful
Repromise.Rejectable
would actually be in practice. For example, if a JS API rejects a promise with two different types, then you have to write custom JS code to bind it. A special, and perhaps common, case of this is a JS API that explicitly rejects promises with some type likeint
, but also raises exceptions that get converted into rejections by JS.Problem: Difficulties typing rejection
Briefly consider fulfillment instead of rejection. In JS, any promise can be fulfilled with a value of any type. You can
race
an array of promises forint
s andstring
s, and the resulting promise can be fulfilled with eitherint
orstring
.In Reason, however, the "obvious" promise API looks like this:
...and that constrains you to racing only promises that can be fulfilled with values of the same, one type. Typing benefits the safety of the API elsewhere, but it makes
race
more restrictive, to an extent not directly related to safety.Nonetheless, the restriction is still pretty reasonable.
Back to rejection: in JS, any promise can also be rejected with a value of any type. The most "obvious" encoding of this is probably what's done in
Repromise.Rejectable
:...except now, you can only race promises that are both fulfilled and rejected with values of the same respective, single types. In practice, this seems way too restrictive and annoying. Rejection is typically a bit of a background mechanism or an afterthought, so it is very bad when interference from typing rejection prevents you from writing a reasonable program.
A problem arises even with
then
:Now, the first promise, representing a "first" asynchronous operation, and the second promise, returned by the callback, have to be rejectable with the same type.
This means that actually taking advantage of the fact that
'e
is a type parameter, and using different types for it, will probably make most code too difficult to refactor or write. For the code to be composable, it will often have to use boththen_
andcatch
right next to each other, in order to be able to vary both'a
and'e
from one promise to the next.Attempt 1: Unityped rejection
This suggests a solution: make one good choice of a type for
'e
, and always use that. This is actually pretty common:exn
(native OCaml exceptions).type rejected = ..;
). I chose not to commit to this because you can't have an exhaustive pattern-matching on an open variant type, and because it's not directly compatible with rejections coming from JS anyway: JS will never generate OCaml open variant values.There are two "degenerate" choices of one type for rejection:
unit
/Top: if we want to propagate rejections using the native JS rejection mechanism, but don't want to bother accurately typing it and don't want to allow values to be carried around by it.never
, so there is no way to trigger rejection of a normal repromise.Attempt 2: Polymorphic variants
Polymorphic variants have the advantage that the compiler can often simply infer the correct rejection variands. However, almost every user of Repromise would likely have to deal with reading and writing either complex type annotations
or casts
In fact, in many places, one is forced to choose one or the other. This seems like a major drawback for a general-use library.
I also considered polymorphic variants for a phantom type to express the distinction between rejectable and non-rejectable promises:
...but it's not clear what is achieved at the cost of such complexity, compared to two separate APIs as proposed now.
One of the problems with such designs is that they make everyone aware of rejection typing. By constrast, without rejection in the main API, hopefully users that stay in the "Reason world" have much less to learn. Only bindings authors need to deal with more complex types in some cases. Even for bindings authors, however, complex types are not very useful. Bindings authors are working with FFI, so they already have the opportunity to write some code in JS and/or assign arbitrary, but straightforward, types.
More background: What is rejection?
This is probably easiest answered by asking "how is rejection different from fulfillment?" Both resolve a promise, but
then
.all
.(1) is properly the job of the
option
andresult
types:We can provide helpers for mixing Repromise with
option
andresult
to model this kind of error handling.(2) seems like just a design decision. Not using native rejection means that if we want early rejection of
all
, we would have to write our own implementation ofall
foroption
and/orresult
types.For (3), Repromise currently forwards all uncaught exceptions in callbacks to a function
which kills the program by default. We should probably change this behavior to just printing the stack trace. A promise that would have been rejected by a failing callback in JS is left pending by Repromise.
We can provide a function like
for catching exceptions in callbacks and converting them to explicit error handling. We could also extend
onUnhandledException
into something close to Async monitors.I'm not yet sure what types to use for exception handling:
exn
?Js.Exn.t
? And, trying to type catching JS exceptions, unwrapping the carried values (of arbitrary type), and using that to reject promises seems like it would create the same unfavorable composability situation as described in the "problem" section.Dealing with exceptions explicitly, rather than converting them to rejection and propagating rejections around, could be a bit burdensome. But, I think the set of programs people write in Reason can be roughly split into two, and exceptions as rejections might not be that desirable or necessary in either case:
The text was updated successfully, but these errors were encountered: