I think that it really isnāt worthy to infinitely bikeshed naming back and forth and thereās precedence for having map and then. If anything I think then and flatThen would just confuse newcomers as much as people already used to FP
Now into the actual problem I see with this proposal: footguns in standard library; I really donāt think that the standard library should have the two unsafe patterns in probably one of the most used types in common JS after arrays and objects. Standard library should at the very least strive for full safety even if the cost is a small runtime check. If 0 runtime is a hard requirement for someone then we can offer an alternative UnsafePromise type that should be used only if the person absolutely knows what theyāre doing
I strongly prefer map and flatMap. I think flatThen is way more confusing than flatMap. Itās the first time Iāve ever seen flatThen, but I guess I could be biased.
I donāt think map and flatMap are problematic at all. In fact, itās a pretty normal convention in the rxjs library, so anyone coming from Angular would probably pick it up quickly.
If we have good documentation on promises and async programming, then it should be simple enough for anyone to pick up.
Since this whole map / flatMap / then discussion is apparently the biggest bikeshed here, I am actually considering removing map and just make it a pure reflection of the JS Promise API instead to prevent any kind of confusion for anyone.
Ultimately this is not supposed to be a FP inspired API, so thereās no question āif we should expose thenā, because this is a hard requirement for following the general zero-cost binding philosophy.
Settling on the most minimalistic approach would probably make things way easier, because this would result in a 1 to 1 translation for the current Js.Promise bindings from pipe-last to pipe-first (with improved error handling).
Itās a shame to see the map function go, but on the other hand, itās easier to add things later on in the future.
ReScript v9 just shipped, so we now can simplify the api even further by replacing Promise.JsError with the builtin Js.Exn.Error instead.
(honestly, thereās not much left thatās there to āproposeā in this proposal, so I guess we can speed up the process replacing the original Js.Promise soon)
It is not planned to add any new functionality to Belt anytime soon. There are some important language related decisions that need to be made first before we can start cleaning up all the Js / Belt modules. Until then, user space libraries are the way to go (which is not bad actually, considering how hard it is making api decisions in core libs).
I want to reply briefly to what appear to be some misconceptions, and also make some arguments against the current proposal. Some of this is in the spirit of @yawaraminās reply about misconceptions about Prometo.
reason-promise does not ātrackā rejections ā thatās only for the low-level bindings API. The main API has a dead simple type promise('a), rejections are impossible, and nesting results in it is fully optional. You can use any other error handling strategy on top of reason-promise.
If I could hide the abbreviation that promise('a) is actually rejectable('a, never), I would ā perhaps itās an argument for some kind annotation so that libraries can hint to the compiler not to show the expanded type. Now that I think of it, there might be a way with a double-underscored internal module.
reason-promise is not a layer over Js.Promise, but a direct binding to JS with its own small (<1K) layer for fixing type safety issues with JS promises and defining all its helpers. The runtime is spiritually the same as what option has for unboxed options. reason-promise could be made trivially compatible with JS promises by setting promise('a) = Js.Promise('a). Then, as long as you choose not to use any of the Js.Promisefunctions, you can use Js.Promiseobjects with reason-promise functions in a type-safe way. The reason (eh) I chose not to declare the types equal, is so that the type system tells users to handle rejections in bindings, and you can be sure that untyped rejections donāt trivially leak into your program. The types are equal in the implementation, however. Only the functions are different.
@ryyppy, this almost weakly implies that reason-promise and/or Prometo have some kind of non-trivial cost. Speaking at least for reason-promise, it has benchmarks, which show that the cost is effectively zero. The actual run-time work amounts to an if-check, which causes no further work in the vast majority of cases. In exactly the one case where there is more work, the case of nested promises:
reason-promise causes one extra allocation to avoid any type problems.
naive promise bindings die ā the resulting objects in the future of the program will have values that donāt match their types.
Given the concern with real-world issue reports, are there any real-world issue reports stating that the cost of an occasional extra if is significant next to the cost of the I/O operations that are performed around promises to begin with? If not, why the goal of zero-cost bindings, rather than extremely-trivial-cost bindings, especially in exchange for type safety? And especially if the cost is the same as the cost of the option boxing check?
I understand the proposed binding has changed somewhat since the beginning of this thread, and the goal has been changed and/or clarified.
I personally would still argue for the basic binding to be something type-safe, that is, a small runtime like reason-promise (like option). I would omit the result layer from it, and leave it for separate libraries.
The reason I would argue for this is because Js.Promise and similar naive bindings are not type-safe in at least two ways:
Nested promises break the types, and, yes, they will eventually occur (I routinely forget which of my functions return promises and which donāt, especially when I am constantly refactoring where I/O operations occur).
Untyped rejections are untyped. Doing them this way amounts to pushing bindings work away from the bindings author and towards whoever ends up handling the rejection in their app code.
Given that a major attraction of ReScript is the type system and type safety, it seems odd that such a basic I/O construct as promises breaks exactly type safety.
The tradeoff, of course, is that bindings are more difficult to write with reason-promise-style promises, in particular when libraries return rejectable promises. However, that is the friction arising from the differences between ReScript and JS and their respective type systems or lack of such, the cost of adapting JS to ReScript, and not pushing the rejection onto the end user. There are already difficulties in writing bindings when JS functions are overloaded, return strange combinations of values, etc. reason-promise style rejection handling adds to the work, but itās the same kind of work and for the same purpose.
Speaking of this untyped rejection, it is not comparable to Javaās unchecked exceptions. Unchecked exceptions are still exceptions, and can be handled with catch, etc. Unchecked exceptions are what OCaml has in type exn. Untyped JS rejections can be any value at all. They are really a sign of an incomplete binding that has mapped values but not errors into ReScript. An unchecked exception in reason-promise speak would be a rejectable('a, exn), which, coincidentally, is also what Lwt.t('a) is. So there is considerable precedent for Java-like unchecked exception-style programming in OCaml (well, all of it is), and itās not broken ā but itās not Js.Promise, which is broken.
Rejections (and exceptions) really need more research. You could look into more detail at Async ā have you? Not sure if there is anything that can be borrowed there.
Itās fair to say that the initial discussion in the thread has shifted towards a simple replacement for Js.Promise, rather than something with the goals of more sophisticated libraries.
For type safety/boxing tradeoffs, something that is perhaps not explianed well is zero cost: it also means the generated code is identical to what youād write by hand. So when eg debugging you donāt need to go look into some runtime lib. The difference w.r.t. optionals is that with optionals, in practice, the special runtime is rarely invoked in generated code (only when the type checker canāt figure it out). We might get there in the future, but weāre not there yet for promises. Also, there are some checks one could experiment with, that prevent you from creating nested promises statically.
An interesting thing I noticed while writing something today is the existence of both ceil_int and unsafe_ceil_int in std, where one of them has a tiny runtime. I think what a lot of people want is to at least mark the new Promise bindings as Unsafe, because well thatās what they are. We can then work on deciding whether including a safe variant in std is something worth tackling or if we should recommend using 3rd party promise libraries.
I personally favour safety and soundness (why I am picking ReScript in first place over alternatives) over tiny runtime hits
And also std should strive to be consistent, correct? If a type or function is unsafe and must be used with care (for instance unsafe_ceil_int) itās marked as such. If weāre including new unsafe promise bindings as part of std eventually then the consistent thing will to be mark them as such, right?
Tried this out today, converting from Js.Promise.t usage; output is pretty much the same with the exception of catch being imported, which seems expected. We generally arenāt using promises yet in our ReScript code but are instead importing the API functions into TypeScript, so it was nice to preserve the āthinā binding to Promise.
One tangential issue I ran into was that gentype didnāt pick up on the ātrueā type being Promise, it instead tried to use this:
import {t as Promise_t} from '@ryyppy/rescript-promise/src/Promise.gen';
Manually typing return values as Js.Promise.t works as youād expect, and Iād assume itāll be fine when it gets put into the rescript-compiler runtime?
@ryyppy I read through your RFC and the comments here and I have to agree with your conclusions. I think the job of the default library in Reason is to bind to JS closely in a (reasonably) type safe way.
This allows others to build on top of it in whatever way they see fit. We for instance actually bind to Bluebird which has fancy mechanisms for propagating stack traces and I will say that you introduce another ReasonML wrapper youāre making stack traces worse.
Yes we want to prevent runtime exceptions but defending yourself against every JS lib you wrap is a full-time job all on its own, itās going to happen any sizable app I would argue (weāve been running Reason in production for years now on the server mostly).
I fully agree with the arguments that ReasonML beginners are in a very fragile state usually, small things such as readable output and familiar APIs will make or break trying to sell it to a technical lead at your company.
There are plans on shipping it in the next few weeks, but probably in a different way than originally planned. We want to take a chance to gracefully improve the Js namespace situation, thatās why it will take a little longer than expected.
Excellent, looking forward to it! Iām planning on transitioning a few things to it, but Iād rather wait until itās in the package itself, so that itās hopefully available within a few weeks.
No plan to add map and flatMap to the API at least as alias to thenResolve and then? I know this is not inside Beltās namespace but inside Jsā where the goal is to keep as close as possible to JS names, but still would be nice to have the choice so you can keep coherent names in your code base.
@tsnobip no plans on doing that, but should be relatively easy to extend the provided Promise module with your own aliases in your app code. Library maintainers need to stick with our then / thenResolve conventions to stay lean.
Also letās wait for the upcoming async story plans before getting back into this bikeshed again
Yeah really looking forward to that, to me itās the last pain point remaining in rescript projects, so much has been done already to make development in rescript an amazing experience!