Belt.Array vs Js.Array in Rescript

You might have noticed that there are several ways in Rescript for iterating through elements of an array. It’s not at all obvious which one you should be using or what difference it makes.

For example you could be using Belt.Array.map or Js.Array2.map or even Js.Array.map ….but I wouldn’t recommend that last one, it’s legacy 🦖🦣

There are plenty more array methods like Belt.Array.forEach and Js.Array2.forEach. Then you have Belt.Array.reduce and Js.Array2.reduce… and we could go on and on.

There’s even yet another type of Array 🫣 coming from the standard lib but to keep it simple for now I’ll not mention it until in the Type safety chapter .

So which one should you use, Js.Array2 or Belt.Array?

There’s no single right answer to that question and developers’ opinions differ a lot. The Rescript community has been discussing how to simplify this as it can be a bit confusing, especially for newcomers. Even though there is no right answer it’s interesting to know the pros and cons of Belt vs Js. In this article I’ll try to list them as well as I can and at the end I’ll talk about our experience at Carla where we write our products in Rescript

Truth be told you can do whatever you like, not default to anything or just blindly choose either Belt or Js and it would almost not change anything. Personally I’ve found that defaulting to either one pays off because it makes your codebase more coherent. It also takes the overhead away, so you’ll not having to think about these kind of things anymore ! Also, you might have a preference for one over the other

We’ll be talking about Array just to have some focus in the article, but majority can also be applied for other data types like Option, Map etc.

The article is aimed to be readable for beginners as well as intermediate programmers - please let me know on Twitter if something is not clear !

Here’s what we’re going to talk about

Table of contents

What are Belt and Js?

Belt vs Js pros & cons

Create missing helper methods

Summary

What are Belt and Js?

rescript_doc

Js module

Js modules contain bindings for all the familiar JavaScript APIs” like [1,2,3].filter, [1,2,3].map and "somestring".length etc. When you use for example Js.Array.includes you’re actually using JavaScript’s Array.prototype.includes .

Bindings (not to be confused with let binding) is just code you write to connect Rescript with JavaScript. If you want to use JavaScript libraries in your Rescript project, you have to write a binding for every single thing you import, whether it’s a function, component, an object etc. It’s just so that we can use JavaScript stuff in Rescript. Those Js bindings are there so that we don’t have to write them ourselves and are included in the Rescript package 🥳

Belt module

In the docs it says that Belt is an “extra collection and helpers not available in JavaScript.“

This explanation is a bit strange when you find out that those helpers are often just the same as you’d find in JavaScript. Belt.Array and Js.Array both have some of the same methods like map, forEach, some. The results are most often same but the implementation can vary. But there are also plenty of other helpers like Belt.Option.mapWithDefault() and many others that you can’t find in Javascript.

Btw. fun fact, it’s called “Belt” because Rescript used to be called Bucklescript and Belts have buckles (ref) and most probably it’s also referring to “tool belt”…

bev-buckle.gif

Belt vs Js - pros & cons

The Rescript docs recommend using the Js module:

rescript_doc

That sounds wonderful, so it seems we’ve just answered the question - always use Js bindings rather than Belt, whenever possible. This was not always the case, before Belt was the default library and many Rescript developers prefer using Belt.

Js bindings

Advantages over Belt :

Disadvantages compared to Belt:

Lets go into more details for each advantage and disadvantage

Performance

Js bindings have zero-cost/zero-abstraction which means that the performance is the same as native JavaScript, it’s runtime free. The output does not contain any bindings code.

Here you see that with Js.Array2 the Javascript output doesn’t have to import anything to run this code:

Screenshot 2022-06-11 at 07.51.30.png

whereas with Belt it imports this file belt_Array.js - so not 0-cost

Screenshot 2022-06-11 at 07.52.56.png

There are however few exceptions to this. Some examples:

  • Js.Array2.get is not 0-cost
  • Belt.Array.getUnsafe has 0-cost

0 cost does not necessarily mean that Js is always more performant than Belt. Sometimes Belt implementation can be more performant than javascript’s implementation.

You don’t really want to be thinking about these micro optimisations all the time. Trust me, you’d be like this at the end of the day 🤯  and not get anywhere. Let’s take an example here:

An example of this Belt.Array.map vs Js.Array2.map. In Belt it’s just looping through the elements, but in Javascript it will first check whether it’s a dense or sparse array (see reference from v8’s blog). However there’s this currying function in Belt.Array.map that might also slow you down a tiny tiny bit, but instead you can use the uncurried version, Belt.Array.mapU. Here just for fun is the code behind the scenes of Belt.Array.map:

Screenshot 2022-07-21 at 22.50.20.png

So it can be pretty difficult to say which is more performant. It depends on browsers and their implementation of Javascript and then you have to check the source code behind Belt. But don’t worry about this, the performance differences are so minimal that in large majority of cases it doesn’t matter really! You might look into it if you’re a library maintainer but even if so, the differences are almost insignificant. If iterating over an array becomes this important you might maybe just as well write a for loop.

Readability

Rescript’s generated output in javascript aims to look as much as just normal javascript you’d write yourself. Rescript’s team has done a pretty good job in achieving this. Because Belt’s library has to import it’s helper, the output looks a little bit less like javascript:

Belt: Screenshot 2022-07-23 at 18.34.45.png

Js: Screenshot 2022-07-23 at 18.36.04.png

Making the output as readable as possible is very important for Rescript’s community to make it more accessible for javascript developers to start using Rescript. Maybe I shouldn’t admit it… but personally I didn’t take much look at the javascript output when I was learning Rescript. So I wouldn’t say the readability would make me prefer Js over Belt.

Beginner friendliness

The main reason why it says in the doc that you should default to Js is because it’s more beginner friendly. When I started out with Rescript I didn’t find Js bindings to be simpler than Belt’s helpers. Most of the times it’s pretty similar and the names of Belt’s helpers are pretty familiar, except for the ones that are unique to Rescript but then they most likely also exist in the Js bindings.

Also the Js binding api is not always so close to the javascript. I haven’t used it but here’s a third party library called https://github.com/bloodyowl/rescript-js that aims at making you feel more “at home” with javascript

Type safety

Here’s an array that has only 3 elements and we’ll try to access an element that doesn’t exist

let a = [1, 2, 3][10]

All right, I know it doesn’t happen every day that we have to access an array like that but it can happen sometimes. Just look for [0] in your codebase and you might be surprised by what you’ll find. When you upload files this is something you might have to use from time to time. And another pretty common case is that you get an array from the backend and you only need the first object.

In JavaScript, when you access an element that doesn’t exist, you’ll get undefined. That can be dangerous sometimes because you’re not forced to handle that case which can lead to runtime errors.

Let’s take a look at three types of arrays and try to access a non-existing element

  1. the built-in Rescript array (without Belt and Js… yes that’s right, there is yet another one 😱)

  2. with Js Rescript bindings

  3. with Belt

Built-in Rescript array

let a = [1, 2, 3][10]

If you run it in the Rescript playground, it will look like everything is just fine ! See playground link. It compiles without errors and warnings ! But in reality if you’d put this on your website, you’d get a runtime error. This was so surprising the first time I tried this because up to that point Rescript had caught almost all of my silly mistakes. I guess it’s easy to get a little bit too comfy and start trusting the Rescript’s type safety a bit too much!

guy falling backwards, nobody catches him

This is the console log error I got when I tried this:

console error

Like you saw in the playground link, it outputs to Caml_array.get(someArray, 10) in javascript.

Here’s the source code from node_modules/rescript/js/camel_Array.js

source code camel array

You see how it will throw an error when the index is out of bounds!

With JS bindings

Here’s a playground using a JS array. The first value translates to the same JavaScript output as built-in rescript example above and therefor it will also give us the same runtime error. But then we could also use the Js.Array.unsafe_get method which gives us false security, it doesn’t throw an error but the value becomes undefined. If we don’t realise that it’s undefined and we’d like to use it somewhere, that could easily throw because like you can see, Rescript sees it as a string:

accessing js array - code

But in reality it’s not a string, it’s undefined and if it’s a value that does not necessarily exist, in Rescript that should be an option. Values of option type force us to handle the case if it’s an undefined value. Is the value a string or is it not? Js.Array2.unsafe_get helper bears that name for a reason.

With Belt

Here’s a playground using a Belt array. To use Belt arrays, it’s enough to just write open Belt at the top of the file

code: open belt at the top of file

If you hover over the someValue you can see the type of it. Here it’s an option, but not a string like in the two examples above!

If you log that Js.log(someValue) you’d see it prints undefined to the console. You might think that it’s just like with Js.Array2.unsafe_get but it isn’t. The reason is that you won’t be able to use the undefined value without peeling it from the option value, you can only log it. So if it’s undefined you’ll have to define some default value, see here. This is the additional type safety we were talking about 💃 You won’t have undefined that can accidentally give you a runtime error

Instead of accessing an array like this arr[0] you can also do Belt.Array.get(arr, 0) (see for more info in the docs). Both will output this javascript:
Belt_Array.get(arr, 0)

Just for fun we can look at the source code. Here you can see the source code behind Belt_Array.get. So instead of throwing as in the source code example above, it’s returning an option.

code: belt returns option

You can write open Belt at the top of the file and it makes standard arrays use the Belt array get function, see an example here. In the docs it’s even recommended to put “open Belt” at the top of all files

Predictiveness

Belt is more “Rescript-y” than Js bindings, in the sense it’s just more of what you’d expect from Rescript and functional programming languages in general. Let’s take a look at few examples…

Immutability

When you’re writing in a functional language you expect it to behave in an immutable way. That’s not always the case with Rescript. When something is mutable it means it modifies the original value, whereas immutable would return a modified copy of the original value. Some functionality in Belt and Js bindings are mutable, like Belt.HashMap and Belt.MutableMap (there is however an immutable alternative, Belt.Map).

But talking about arrays, some of the helpers are mutable in both Belt and Js.bindings like for example Belt.Array.fill and Js.Array2.fill .

There are some helpers in Belt that are immutable whereas in Js bindings it’s mutable, for example Array.sort and Array.reverse

Belt Js bindings
Arr.fill - mutable Arr.fill - mutable
Arr.sort - immutable Arr.sort - mutable
Arr.reverse - immutable Arr.reverse - mutable
Arr.shuffle - immutable doesn’t exist
Arr.shuffleInPlace - mutable doesn’t exist

Arr.shuffle doesn’t exist in Js bindings nor in pure javascript but here Belt offers us both an immutable helper and a mutable one. Sometimes there’s also a mutable option because it offers slightly better performances.

Argument order

In Rescript you’d always expect the argument order to be data-first

If you’ve never heard about this concept, here’s the difference in a made-up programming language just to demonstrate the concept.

let numbers = [1, 2, 3]
let dataLast = Array.map(a => a + 1, numbers)
let dataFirst = Array.map(numbers, a => a + 1)

All the Belt bindings are data-first but in the Js bindings this is not always the case, as says here in the docs:

“For historical reasons, some APIs in the Js namespace (e.g. Js.String) are using the data-last argument order whereas others (e.g. Js.Date) are using data-first.

For more information about these argument orders and the trade-offs between them, see this blog post.”

Also the legacy Js binding Js.Array is data last whereas Js.Array2 is data-first. Later they’ll probably remove Js.Array and Js.String and all the other helpers will become data-first.

Js bindings !== Javascript

Let’s take a look at this rescript snippet and don’t scroll further than this snippet if you don’t want to see the answer!

let someArray = [{a:"a"}]
someArray->Js.Array2.some(a => a == {a:"a"}) //true or false?

in javascript, this would be false, but here it is true! It’s a bit unexpected but it’s completely normal because the code inside the callback isn’t javascript but rescript. We’re using the == rescript operator inside the callback, that does not work the same way as it does in JavaScript. Well you’d have the same results with Belt but with Belt you wouldn’t necessarily expect it to behave as javascript.

Create missing helper methods

Like you saw above, some functions are unique to Belt and others are unique to Js.

venn

But if you choose to default to either Belt or Js, then you can always create those methods in some other way

Here’s an example where we’re creating the includes method for Belt using Belt.some(). We can use the opportunity and make it behave in an expected way, comparing content instead of references.


let someArray = [{a:"a"}]

//custom Belt array method for .includes
let includes = (array, a) => {
  array->Belt.Array.some(b => a == b)
}

let isIncluding = someArray->includes({a:"a"}) //true

You could also create your own library with Belt or Js functions and even write some of your own as well. At Carla we usually have a bunch of extra helpers like StringExtra.isEmpty, StringExtra.capitalize and many more. Doing this could help you decide on scoping down which helpers you want to use

Summary

It’s difficult to compare every single aspect of Js and Belt but we’ve roughly touched on the biggest differences. Now we’ve outlined that Belt can give us more type safety than Js.Array and also it’s closer to how we would expect Rescript to work. Js bindings outputs a bit more readable javascript than Belt

At Carla we’re building both a backoffice for internal use and also a storefront for our clients. We default to Belt array helpers in our backoffice where type safety is very important and Js array helpers for the storefront page where performance and SEO is more important. We could just as well have reversed this…Our backoffice would not be significantly more performant with Js and up till this point I don’t think there has been any runtime error in the storefront because of us using Js bindings. I think it still makes sense though to default to Js bindings in the storefront because we have javascript in there and a team of javascript developers working very close to the Rescript parts of the code.

What you might prefer depends on so many things, if you’re a library maintainer or migrating from Javascript to Rescript etc. I hope this reading has been useful and don’t hesitate to let me know if you have some comments, ideas to improve this article or some questions! :)

🙏 Thanks to my dear amazing colleagues who encouraged me finishing this article, sanity checked it, and gave me plenty of inspirational ideas, Robin, Timon and Dmitry


Get more tips in my almost monthly newsletter about CSS & React!