Roles for Belt,Js, and Pervasives in ReScript

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

I am not a big fan of using open. Is that lots of typing compared with below?

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

If Belt is affordable, that is the only lib you need, it is actually more performant than JS, just a bit more intrusive than JS, so Js may fit a niche use case when you want to convert existing Js code into ReScript

2 Likes

I used to use belt as it is (not opening via compiler flag) but i ened up creating lots of aliases and opens at start of expressions / modules.

1 Like

This is what I was talking about in my point. Most of Js is runtime free, but there are also parts of Belt that are optimized and necessary for writing clean JS, like Belt.Array.getUnsafe mentioned in another post.

Js and Belt are very linked, to the point where it is confusing to beginners to the language (where I count myself in). I’m sure if you are used to the distinction and the names it feels like it is not a problem, but it is a big unnecessary detail.

For example, like knowing that for working with arrays here is the array type globally available, there is Js.Array2, and there is Js.Vector (??? just learnt about this), and there is Belt.Array, all with different functions, some that are just like the Js functions, some that are mutable utilities, some that treat the array as immutable. This is a lot of incidental complexity that is fully self-created, and if you are cleaning up, it is the time to address it.

Think about who you are targeting, is it converting JS users? If that is an important audience, think about what they do. For example with Array:

  • There is one array type
    • Solution: Unify all the array modules to make it easy in the autocomplete and docs
  • I call functions on it without even knowing the module type
    • Solution: Well it is not possible to do this, but reducing the api surface to one name xs->Array.length is the shortest we can do.
      Anything longer, Js.Array2., Belt.Array, Js.Vector all lose in length to `` which is the prefix JS developers have to use to call an array function.
  • To know if an Array function is mutable or immutable, as a Js dev I rely on the docs mainly
    • Solution: Great, we have great docs. We can also split the functions in the docs by mutable / immutable, and on each function clearly specify if the array is mutated in place. We also have the return types which are a good indicator of the mutability. Highlight this.

Anyway just an example, and my perspective. You can always get used to the idiosincrasies of each language but if you are cleaning up with breaking changes, it may be worth rethinking the design in a more holistic way. Maybe it is just my Elm experience showing here, the standard library is a pleasure to learn, very well designed and documented.

6 Likes

Yes that would be a great idea!

I don’t know if it’s worth the trouble. Adding a library dependency is trivial in ReScript. You might just go ahead and remove the Stdlib library altogether once you are confident you cover everything. Anyone who wants it back can install it separately.

2 Likes

Maintain the legacy stdlib is non-trivial work, there are lots of C shims to support in the compiler, it can not be maintained by 3rd party.

As we discussed, the retirement plan for ocaml stdlib would be that we first have a flag to allow users to explicitly opt-out; later opt-out by default and final removal

1 Like

Reading all that, I think that a -stdlib-future flag would be great.

I’m currently experimenting a bit on what would make a newcomer experience easier by only exposing a subset of Pervasives, and I came down to the following module: rescript-js/ReScriptJs__Pervasives.res at main · bloodyowl/rescript-js · GitHub

I also think that, apart from the internals like Obj.camlEqual, CamlInternalLazy, internal int32 and so on, this flag could only make it so that only the following modules would be exposed:

  • Lazy
  • Int64 (while BigInt still has some light support, but that could be implemented user-land)

What do you think?

3 Likes

Can you go to the pervasives module and mark functions not needed as deprecated, or we put it in a separate module, e.g Belt.Float for lots of float operations in Pervasives?

-stdlib-future is to ensure that none of the legacy stdlib will be exposed,
we can provide more polished APIs in Belt.Lazy/Belt.Int64 etc.

Contributions are welcome

1 Like

I’m in favour of this, although the OCaml standard library doesn’t impact my project too much. Some context may be useful for the discussion.

The project I’m working on has a stdlib replacement combining elements of Belt and Js along with some custom sprinkles and renaming methods to match existing libraries in our main TypeScript codebase. It may even be open source by the end of the year.

The only files that don’t have open Tiny at the top are the ones the stdlib depends on. If/when we’re able to switch to pinned packages in our monorepo we’ll consider something like "bsc-flags": [ "-open Tiny" ] or similar in the individual package bsconfig.json files.

2 Likes

Relevant GH issue: Add BigInt primitive number type · Issue #4677 · rescript-lang/rescript-compiler · GitHub

When is getting rid of default ocaml library planned? I was already confused by it when I used some arrays.

See the Roadmap

In ReScript version 11 which will be next year.

Thanks. Can it be deactivated already with some compiler flag?

Not directly, but there is a bsconfig setting to exchange the built-in std lib with an external one:

"external-stdlib" : "@rescript/std"

It is probably possible to exchange the @rescript/std with your own minimal one.

The easiest way to not use the OCaml standard library is to open Belt in each module. Belt will shadow a lot of the OCaml modules like Array. (Although it won’t replace all of them. You probably need to use Js.String2 instead of the built in String module, for example.)

You can configure rescript to automatically open Belt for every file by putting this in your bsconfig.json:

{
  "bsc-flags" : ["-open Belt"]
}
2 Likes

Yep, that is indeed a solution.
I guess that, after all, no runtime is just a “selling point” which, while technically possible, is not realistic

3 Likes