Roles for Belt,Js, and Pervasives in ReScript

Hi all,

We had some discussions around the roles for such libraries, below are some guidelines:

  • Belt would be the most comprehensive library, if you can afford Belt, Belt is the only library you should rely on. There are some missing features in Belt, we plan to add it later, contributions are welcome.

  • JS is planned for migration from Js to ReScript, so it is mostly external bindings, it is in theory less intrusive than Belt when introducing ReScript into your existing codebase, contributions to more JS APIs are welcome.

  • Pervasives/OCaml stdlib. This is the library we inherited from OCaml, it will be kept as is. We may put it under a separate namespace called Caml.

8 Likes

My 2 cents:

  • almost anyone developing apps wants to have one standard library. We, for one, ended up with a bunch of modules that include Belt modules and add some functions (some functions use JS modules, because we don’t want to use JS in the app code directly)
  • JS might make sense for lib authors (because apps usually can afford Belt)
  • Caml: should ReScript even expose its OCaml legacy? Or is it just for backwards compatibility?
3 Likes

agreed, OCaml stdlib could probably be either removed or at least placed behind a namespace, it’s the least optimized of all 3 but the most accessible right now.
If the OCaml stdlib is removed or moved to another namespace, shouldn’t Belt be opened by default?

3 Likes

Yeah these are the crucial details we still need to figure out.

If Belt would be opened by default, it would mean that Belt, as a term, would completely disappear, since we are now referring to the modules as List, Array, etc. This might be confusing. It might also be cumbersome for cases where users want to actually open Js by default to get access to JS idiomatic bindings like Array and Dict. I’d prefer the latter for many different scenarios → use zero-cost by default, but use Belt explicitly as a proper stdlib if needed.

Also, as @hoichi correctly stated: Library maintainers would probably want to use zero-cost APIs over Belt.

^ Note: We want to take the chance to finally fix the confusing Js.Array, Js.Array2 situation as well (e.g. getting rid of the pipe-last versions of the zero-cost bindings / remove Js.List / adding the new Promise bindings). This will require careful coordination, since we want to allow (automated) migration to the new core apis (or at least don’t break existing users during the transition phase).

The most promising idea was to iterate on the new Js implementation outside of the compiler (pretty much what @bloodyowl did with rescript-js). More concrete plans and details will follow soon (hopefully).

Ideally it shouldn’t be exposed by default so it doesn’t randomly show up in auto-completion. I could either imagine having a configuration flag (for “ocaml legacy mode”), or just make Caml.ml available as a curated library… no matter what, our users will be able to use the original APIs for backwards compat.

3 Likes

I think one of the more confusing set of modules is Array / Belt.Array / Js.Array(2). The Belt and Js versions offer a lot of redundant functionality, so it isn’t often clear which one “should” be used.

Making it more complicated is the array[index] syntax, which desugars to “whichever Array.get happens to be in scope.” It creates confusing situations like when the OCaml Array.get raises exceptions or when open Belt turns all of your values from arrays into options and creates lots of type errors you need to fix.

6 Likes

Yes, but library maintainers might be the minority, so maybe they could do with Js.Array etc.?

Yikes. And speaking of arrays: are any plans for immutable arrays? Belt.Array has set, so the type is effectively as mutable as the Js.Array type.

2 Likes

I just want to share my perspective since it seems to differ from other commenters.

Typing e.g. arr->Js.Array2.map is pretty verbose when compared with JS’s arr.map. Replacing “Js” with “ReScript” seems like it would exacerbate this. Preferrably, I just type ->Array.map.

My ideal would be to have Js.* open by default. This seems to align with ReScript’s philosophy as it has minimal runtime and API surface area.

I think users who want to avoid Belt entirely should be able to do so (ideally, being able to exclude it from the project entirely so that others on the team cannot use it). It should not get any first-class treatment, but instead be treated as a 3rd-party library, like lodash.

I think arr[X] should return option() by default, and users should have to use some special operator/function to do an unsafe get.

3 Likes

are you aware of this?

let {map, filter} = module (Belt.List)

xs -> map (...) -> filter (...)

It is generally recommended to use qualified import instead of open

10 Likes

He is probably not aware of this because it’s neither documented, nor did I, nor anyone else on the team ever use that feature yet. I am not even sure if we highlight that particular syntax consistently in all our different editor plugins right now.

Our editor support autocompletes to ->Js.Array2 etc, so it would certainly be nice if it could just expand to ->Array instead.

4 Likes

Would it be possible to in the future to not auto-open the OCaml stdlib and put it under Stdlib. This would save many mistakes by using the wrong Array module for instance.

4 Likes

Good to know, thanks. Although this trades off ease of refactoring (if I decide I want to remove filter, or add a reduce, I need to go update the import) for the extra concision, so I personally would prefer using the fully qualified names.

I am not even sure if we highlight that particular syntax consistently in all our different editor plugins right now.

It is not a new syntax, it just works.

I think it may be easier for editor plugin to implement auto-completion for map compared with Array.map, since map is explicitly in scope

cc @cristianoc , let me know if I am incorrect.

Based on the discussions we had, introducing Caml is to provide a flag to not break existing code, the ultimate goal is to remove it.

The caml stdlib does not make too much sense in JS backend

  • It’s slow, not stack safe, not exception safe and generates large js bundle size. (Note such downsides may not appear in native, but that’s not what ReScript focuses)

  • To maintain the stdlib, we also need maintain its runtime support.

I think that’s a good plan for what to do with the Ocaml stlib. The tricky thing is to decide whether to make Belt or Js the default. Could this be configurable too as there seems to be a definite split in preference.

Yes. Removing it would be even better. I thought you might wanna namespace it to provide backwards compatibility for some people who still might be depending on it?

If you keep the auto-open of Stdlib I think it will be more confusing and more error-inducing for 99% of the people than leaving it out (or keeping it under a namespace).

It does not matter to the editor plugin.

1 Like

My 2 cents, because I haven’t seen this perspective mentioned:

There should only be one standard library, and it should be open by default. The name should hidden and transparent to users of the language.

Anything else is extremely confusing for learning the language, how the documentation navigates and reads, and the readability of the code.

So many mainstream languages have a standard library that is readily available and not namespaced or opt in. From my following the language from the sidelines, it seems like Rescript has inherited the very confusing OCaml situation of having so many standard libraries and options. As a non-OCaml user, I’ll remind you, this is very confusing specially to beginners in the language.

If there is a need to clarify which functions are low-cost Js interop, or treat the data structure as mutable / immutable (Js.Array2.* vs Belt.Array.*), this should be done in the documentation of the functions for editor use, and in the website by having sections / separations between them and the code docs rendered.


I’ve personally been using “-open Belt” in my bsc-flags, and I’ve also had to many times do aliases like

module String = Js.String2
module JsArray = Js.Array2

Because I find Js.Blah2.bleh is very unreadable in the code. I also have to use JsArray because I use both Belt.Array functions as well as push and others from Js.Array2.

Very confusing overall :stuck_out_tongue:

4 Likes

The key question is: do you include batteries in your stdlib or not?

One philosophy is to provide a standard library which is quite large and provides a default set of tooling for the problems of the day. A non-academic data point: “does the standard library include a JSON encoder/decoder?”

Another philosophy is to have the standard library provide the bare minimum, namely things that can’t be implemented in the language directly. Your language might not have specific notation for arrays say, and you bounce that into a library. Yet, it is hard to implement arrays without some help from the underlying runtime for efficiency reasons. So that gets included in the stdlib.

OCaml, and also Javascript, generally falls in the latter philosophy.

It is a trade-off as well, with strengths and weaknesses. For instance, if most of the stdlib lives independently of the compiler, you can avoid synchronization of releases, which is a nice thing. On the other hand, you risk having multiple implementations of the same thing, and different libraries end up cleaving and isolating the community in multiple groups.

I’m more familiar with Janes Street Core libraries:

  • Base - Absolute minimum. High portability. Extremely high stability.
  • Core_kernel - Extends Base. Pulls in some dependencies, more features, High stability, but the API morphs more.
  • Core - Extendends Core_kernel. Adds UNIX APIs.

This layered approach is nice because packages can “graduate” into the kernel once they are good enough: Belt.String, Belt.Date, Belt.Promise might be examples.

4 Likes

I have some better ideas to make OCaml stdlib not accessible with a flag without introducing a separate namespace.

So the plan is to have a flag called -stdlib-future, that will only make Belt and Js accessible. It will be off by default for some time to make time for people’s transition, then on by default, after some time, we then remove the legacy stdlib, let me know what you think

4 Likes

About what stdlibs should be accessible: I like the idea of being able to configure in bsconfig.json which standard libraries shall be available. We could choose among:

  • js: new unified/cleaned up Js module
  • js-legacy: old Js and Js2 modules
  • belt
  • ocaml

To match the current behavior, the default would be:

"stdlib": [ "js-legacy, belt, ocaml" ]

As a library author, like @ryyppy suggested, I might like to make sure that only Js is accessible, so I would set:

"stdlib": [ "js" ]

As an application author, I would like to make sure that I am not inadvertently using something from the OCaml stdlib, so I might set

"stdlib": [ "js-legacy, belt" ]

for an existing project or

"stdlib": [ "js, belt" ]

for a new project or even, once all functionality available in Js is also present in Belt

"stdlib": [ "belt" ]

As for what should be opened by default: It is already possible today to globally open Belt by specifying

"bsc-flags": [ "-open Belt" ],

in bsconfig.json.

Maybe, if only one stdlib was specified, it could also be opened automatically.

4 Likes