[RFC] New Promise Binding

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.

9 Likes

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.

4 Likes

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.

3 Likes

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

11 Likes

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)

3 Likes

I’d be interested to continue this in a new thread if you are willing to discuss.

2 Likes

I would love to. Might be a useful discussion to have

2 Likes

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 :slight_smile:

2 Likes

Thanks for the explanation ryyppy. Evaluating this as replacement for Js.Promise rather than reason-promise makes a difference :slight_smile: I was making the latter comparison.

I’m still in the map/flatMap camp but will refrain re-iterating what people have said above.

3 Likes

I’ve worked on an alternative take a few months ago to handle some pain points I’ve had with Js.Promise.

  1. 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.
  2. missing map/flatMap, but those two are addressed in the proposal :slightly_smiling_face:

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.

1 Like

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 promise
  • map 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.

6 Likes

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.

1 Like

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 :man_shrugging: Let’s try to converge, not diverge :pray:

11 Likes

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 :man_shrugging:.

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.

8 Likes

Belt uses the flatMap 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 :slight_smile:

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.

3 Likes

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

5 Likes