It’s also available both in Chrome and in FF, but that per se doesn’t mean it’s widely used. Modern JS APIs are huge, so I’m not really surprised if some methods are used by less than 0.1% of frontend devs.
I think this has been touched on, but just to be clear and manage expectations: this proposal is supposed to be a low-level replacement for the Js.Promise
module, and not a replacement for reason-promise
, prometo
, etc? Users can continue to use those promise libraries without anything changing? And, in the future, new promise libraries will presumably build on top of this proposed API instead of the existing Js.Promise
?
I think the comparisons to reason-promise
may have given the impression this was supposed to replace it. If that was the case, then it would be obviously lacking. As far as a Js.Promise
replacement though, then it seems like a nicer API. (Based on just glancing at it. I may have more feedback once I use it in practice.)
I agree that a flatMap
function would be nice, even if it was just an alias for then
. The comment that flatMap
isn’t familiar to the JS world seems weird because: A) flatMap
does exist in JS and B) flatMap
is well established in the libraries that already ship with ReScript. I assume that consistency within the ReScript stdlib should be a priority.
Yes, that’s correct. It is aiming to replace the existing Js.Promise
bindings as the low-level primitive that all the other libraries such as reason-promise
and prometo
are using.
That’s probably the most realistic suggestion, having both functions with an alias (maybe with the other having a deprecation warning to point to the other function). Other alternative would be to remove the map
function alltogether, then there is no point of confusion that there might be even a flatMap
.
Honestly, my bigger concern is currently usage patterns. When telling someone to start from scratch, doing multiple async operations and chaining them together, how would they do it?
- Does the type system yield user friendly errors?
- does the promise nesting edge case cause problems?
- Is error handling (ReScript errors / JS errors) approachable and easy to remember?
Or maybe another scenario: The user sets out to bind to a third-party js library function, creating a promise, and using it in the existing system. Is this intuitive and easy to do? Is there anything missing that prevents them from handling specific error scenarios, or other edge-cases (e.g. some promises may throw a JS value instead of an Error… does the user know how to access that error)?
These are really important questions I’d like to see answered at some point.
OK, if this is intended to go in Js
, then I agree that it should have (almost) no runtime. I had assumed it was a more opinionated ReScript approach, in the style of Belt.
I know that this might be considered “bikeshedding” but I’d like to voice my concern with using then
especially for “js users” sake.
I might be missing something obvious but is there any common understanding that “js crowd” perceives then
as flatMap
? I’ve been using @ryyppy’s repo lately (this commit where then
is actually map
) and I can say that there is no way out of confusion for “fresh js crowd” with Promise
bindings if then
is used.
Some people will expect then
to be map
some to be flatMap
and both functions will have to have a “Note:…” in the docs. Why not just skip the then
to remove any chance of confusion all together. If one doesn’t use those functions when working with options
they can learn (map
is then
when you don’t return a promise and flatMap
is when you do) them instead of overloading the then
.
It’s also a good idea to consider that some people write in both rescript and js (sometimes js for work and rescript while learning). Overloading then
might not be that good in this case.
Ironically I would even prefer then
and flatThen
no matter how weird they might seem. Those names are clearer than then
and map
On the subject of adding result
to the bindings I agree with @ryyppy decision. I used reason-promise quite a bit and in our case it actually did more damage when the codebase grew. Most of the time we’d have to get back to simple promises and create our own “monad” stacks on top of it.
Some thoughts on “over”-using result
I found in the wild (just opinions of course, also f# , useful nonetheless)
I’d be interested to continue this in a new thread if you are willing to discuss.
I would love to. Might be a useful discussion to have
Scott Wlaschin’s recommendation to only use Result
for domain errors makes a lot of sense to me. But yeah, I’d read that discussion
Thanks for the explanation ryyppy. Evaluating this as replacement for Js.Promise rather than reason-promise makes a difference I was making the latter comparison.
I’m still in the map
/flatMap
camp but will refrain re-iterating what people have said above.
I’ve worked on an alternative take a few months ago to handle some pain points I’ve had with Js.Promise
.
- Not being able to type errors correctly (as it only has a single type parameter): that makes me use Futures containing
result
in all of my codebases. I’d personally favor that over retro-compatibility. - missing map/flatMap, but those two are addressed in the proposal
Here’s are the bindings: https://github.com/bloodyowl/rescript-js/blob/main/src/Js__Promise.res
I love this proposal, thanks! I think it’s great to keep it simple and if you have more complicated use cases, there are other libraries. We don’t really need more than this for a medium sized app, so I suspect this should be fine for a lot of users!
I was a little confused on the difference between map and then (because in JS then can be both). I suspect flatmap will make it easier to understand even if it’s not the same naming as JS.
I am probably in the minority here as I have minimal FP knowledge. I had the opposite experience to some others in the discussion so far.
When I read the docs:
then
is being used to provide a callbacks that returns another promisemap
is being used to provide a callback that transforms a value within a Promise chain.
Along with the examples, this was clear to me. But I now understand why it might be confusing to some.
Clearly a lot of thought has gone into the current proposal and I assume a principal being followed is that when binding to JS APIs we stay as close to those APIs as possible, even if it’s at the expense of using correct FP terminology?
Very few of the JS devs I’ve worked have had any FP knowledge. If I was trying to sell ReScript into these teams then familiar terminology is a significant selling point. In fact promises with then
and catch
are so familiar than any deviation from that would be hard to explain.
To be honest, I didn’t understand why flatMap
was being proposed as an alternative to then
. I’m familiar with flatMap
for the JS arrays, but I was unclear how that related to promises. I’ve since done a bit of research and I think I now understand how the types for then
are actually flatMap
?
Just wanted to share a perspective that I feel has not been represented much in the discussion yet. Hope it’s helpful.
Great work, I think the new bindings look very good. I’d probably advocate for an alias of then as chain and flatMap. The name chain is more idiomatic in the JS world and used by ramda, fluture, and Fantasy Land, which is a spec for JavaScript FP libraries. There are quite a few packages which follow this spec, and many looking to do FP in JavaScript reference libraries here which implement the Fantasy Land spec.
I don’t think a deprecation warning is necessary to make one more preferable than the other. In terms of documentation though, I would use then in code examples as that’s going to be better understood by a wider JS audience.
If this is going to a zero cost abstraction the I see why you’d want to raise and catch, but ultimately I’d prefer to see a Belt.Promise that uses Result as I believe that would be more idiomatic in ReScript.
In any case, good stuff and thanks for working on this.
I don’t think multiplying the aliases is a good idea though. Belt
uses the flatMap
convention already, so I think all ReScript libs should try to use that. Not chain
, not bind
Let’s try to converge, not diverge
I was thinking about also using then
and flatThen
as aliases for map
and flatMap
. (So then
would be the opposite of the current binding’s then
.)
Could be more intuitive, or maybe not .
Although, now that I’ve tried out the proposed binding, the difference between then
and map
seems easy to get used to in practice. Since I’m already aware that promises are quirky anyway, I don’t really mind that they look slightly different than other monadic types.
I said technically it’s a flatMap, but actually it’s both, a map and flatMap, which doesn’t fit into our static type system and makes it rather special. That’s why I originally experimented with an api that was a little bit unconventional: then
for transforming a nested value, and flatThen
for chaining promises. After getting some feedback from other users, I switched that back to the design we have right now.
I recently got some more feedback from other community members that prefer the then
api as well, but aren’t that vocal on this forum (some are production users using ReScript in frontend / node and their own custom promise bindings; some are interested JS newcomers that joined my live coding stream last week, etc.).
So my final thoughts on why I think we should settle on then
:
Async operations are probably one of the first things newcomers are going to attempt when e.g. trying out rescript-react, otherwise their UI will be rather useless. If this one flatMap
will cause confusion for just one user, then it is very likely that there will be a hundred / thousand more that will fall into the same trap. Non-ReScripters that haven’t even used ReScript, will have this extra mental mapping while reading getting started blog posts, or when reading their first ReScript code a coworker introduced for assessing the technology. Even if it doesn’t sound like much, a flatMap
magically turning into a then
in the JS output is non trivial mental mapping. I know that I am kinda contradict myself here because I added a map
function, but at least map
is a little bit more familiar due to way more common array map usage. Hopefully the first thing ppl will try is Promise.then
anyways.
Regarding Belt: It is not the very first API users are going to use, they will most likely be using Js.Array2 / Js.String2 etc. first, because it’s closer to the JS apis for the beginning. When they are ready, they will be using Belt for more advanced stuff presumably. From a user-journeys view, first we need to get ppl use the technology without much pain by introducing familiar apis. Later on, when they are committed, it’s way easier doing the extra work teaching about ReScript specific conventions.
The team’s opinion right now is trying to stick to following mantra: when in doubt, stick with JS conventions / names, especially when we talk about zero-cost bindings.
Now, I hear your complaints on “why are we always focusing on the JS newcomers”, and the reason is very simple: Because if an API is simple enough that a complete newcomer will understand, then chances are very high that a more advanced users will understand it as well. Sure we could teach them some ReScript conventions during the onboarding, but who’s got time for that? It’s a death by a thousand paper cuts.
From previous API design experiences, I want everyone to think back to a time where reason-react
utilised a record-based component api. Can you remember how unintuitive the API looks in comparison to today, even though it was not that complex to learn? Ever since the React bindings follow the 1-to-1 name mapping approach, it unlocked incredible potential on the teaching and interoperability front. Not being forced to constantly map back and forth between JS / ReScript naming and types is an incredible feature a lot of ppl take for granted.
Belt
uses theflatMap
convention already, so I think all ReScript libs should try to use that
Ah yes, I wasn’t thinking about that. Makes sense - although sometimes I wonder if the Js module should follow JavaScript idioms and Belt follow OCaml/ReScript idioms, given that its more opinionated.
One downside to flatMap and chain is that I would expect the inner implementation to use join and map internally, which obviously isn’t possible for promises.
Anyhow, I don’t have a strong enough opinion to push much on this. I’m fine with using then, but in my head, I’ll be thinking chain. If we have a Belt.Promise module though, I would would hope to see flatMap there for the reason you listed above - to keep consistent with other Belt modules.
Well, you’re right, these are standard API bindings, and they could have their own conventions.
An obvious problem is that you use a JS convention and call a function then
, you still need another function, no matter how you choose to call it, and I bet this will still trip up people coming from JS (“Why do I get this type error? This was fine in TypeScript”).
As an aside, I’m not sure I fully buy the “let’s be as familiar to the JS crowd as humanly possible” argument. For one thing, there are bigger WTF moments for newcomers than Promise.map
and Promise.<that_other_fn>
. For example, modules like Belt.Foo2
, or rather their obsolete t-last siblings
More importantly, people can learn, and in the long run, what matters for keeping the mental overhead low is consistency. If everything is t-first and uses names like flatMap
and forEach
(personally, I’d prefer bind
and iter
, but it’s not important), you have more capacity to think of the essential complexity.
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