This article was originally written with ReasonML. I updated it in May 2021 to use ReScript.
For a JavaScript developer learning ReScript , one obstacle is getting the hang of container types. ReScript’s standard library, Belt, has docs but they aren’t friendly to new users. They don’t clearly explain what the containers are, how they work, or why you would use them. Fortunately, they also aren’t difficult to understand once you can grasp the basics.
Coming from JavaScript: the abused object type
In JavaScript, we use the object type for just about everything. In many cases, we use it like a ReScript record: a container with fixed keys and values that each have their own type. In other cases, we use it more like a map: a container with a dynamic number of keys that are each “mapped” to a value. The ReScript docs explain this distinction in detail.
In ReScript, these two uses are completely separate structures and cannot overlap. ReScript is designed around managing both cases. For static objects there are records, and for “map” objects ReScript provides the Js.Dict
module.
If you use these a lot, you’ll quickly notice that they come with a bit of friction, awkward code, and missing features. You can’t easily do immutable updates, for example, or merge objects with spread syntax like you would in JavaScript.
This may seem like a downgrade compared to regular old JavaScript, but that's not the whole story. ReScript comes with a standard library, Belt, which uses its own data containers. Most likely, you want to use these in your project instead of Js.Dict.
OCaml data containers
Before we get started with Belt, it helps to understand that the Belt container types are all based on the OCaml standard library containers (in fact, you can use the OCaml standard library in ReScript, but Belt is designed to be more efficient when compiling to JavaScript). Because of this, a lot of documentation you can find about OCaml containers will be applicable to Belt as well. For example, the official OCaml website has a great comparison of the different containers you can see here. The book Real World OCaml also has in-depth guides to using them. While the code that those websites use isn’t exactly the same as Belt (and it’s in OCaml, not ReScript), the concepts are mostly the same.
And now, let’s jump into what the containers actually are:
Map
For simplicity, let’s first look at one of its inner modules: Belt.Map.String
.
Belt.Map.String
is analogous to a JavaScript object that’s used to “map” values (or the actual JavaScript Map
object). Conceptually, it’s a series of key-value pairs. The keys are strings, and the values can be any type you want as long as they’re all the same type.
If you want to use a map in your code, you may think the first step is to create an empty one. Surprisingly, there is no make
function for Belt.Map.String
. Instead, you simply use the Belt.Map.String.empty
value (it’s not a function!). Because maps are immutable, you can reuse that value every time you need a “new” empty map.
If you’re not familiar with immutability, keep in mind that when you assign a value with a function like Belt.Map.String.set
, it returns a new map that has the new value. The original map will be unchanged. (Due to the way maps work behind the scenes, this is actually very efficient.)
In JavaScript, object keys are strings, so Belt.Map.String
will probably be most familiar at first. But you can also use Belt.Map.Int
, which uses (can you guess?) integers as keys. That makes it more akin to a JavaScript array.
(You may wonder, why would I want to use an int map instead of an array? Immutability may be one benefit. Also, with a map, you can use negative integers as keys. You can also assign a value to 0
and then a value to 99
, and you’ll get a map with just those values. In an array, if you do the same, you’ll get an array with 100 entries and 98 of them are undefined
.)
HashMap
On the surface, Belt.HashMap.String
(or Belt.HashMap.Int
) works similarly to Belt.Map.String
(or Belt.Map.Int
). One key difference is that hashmaps are mutable. When you assign a value with the function Belt.HashMap.String.set
, it modifies the hashmap in-place.
In functional programming land, immutability is preferred, but structures like hashmaps are useful too. In some cases, a hashmap is more performant than a regular map. Both come with benefits and trade-offs, and both serve different purposes.
You’ll notice that there is a Belt.HashMap.String.make
(and Belt.HashMap.Int.make
). They take one argument: hintSize
. That’s just your best guess as to how many keys the hashmap will have. The closer it is to the actual size of your data, the more performant your code will run. You don’t have to stress about it being exactly right.
Set
The Belt.Set.String
and Belt.Set.Int
modules are conceptually similar to a JavaScript Set
object. They store a series of values that have to be unique. If you try to add a duplicate value, then the set will be unchanged. Like a Belt Map
, sets are immutable, so you usually don’t “make” them, just use Belt.Set.String.empty
(or Belt.Set.Int.empty
) instead. Updates will return a new set without modifying the original.
Just the beginning
Most of the time, especially coming from JavaScript, these container types will suit your needs, but there are other ones too. Belt has “stack” and “queue” containers, plus variations like “hashset” or “mutablemap.” Other standard libraries usually have their own container types, too.
Next steps: making custom containers
You probably noticed that I skipped the Belt.Map
, Belt.HashMap
, and Belt.Set
modules and only explained their respective String
and Int
inner-modules. The outer-modules let you use your own custom data types as your keys. (The String
and Int
inner-modules largely exist for convenience because using strings and integers as keys is so common.)
Setting up your container with custom keys is a bit more complex and utilizes some advanced ReScript features. That will require a blog post for another time. Stay tuned!
Top comments (3)
Thanks, that was helpful.
It's difficult to grasp for beginners where to find the right tool. There are the Reason docs (which are a good starting point), then there's BuckleScript (terse and to the point), and Ocaml (as the last fallback).
Unfortunately, I get the sense that it's somehow required to know all the libraries/tools to solve a problem.
Never heard of
Belt
containers. Thanks for the post.Thanks for the article!
Would also appreciate some examples here.
Also, they finally have more user-friendly Belt documentation:
reasonml.org/apis/javascript/lates...