This article was originally written with ReasonML. I updated it in May 2021 to use ReScript.
In my previous post, we reviewed the basics of how Belt’s containers work. Most of the time, especially coming from JavaScript, you’ll just need to use Belt’s String
or Int
inner-modules (e.g. Belt.Map.String
). As you get more comfortable with ReScript, you may find that these types aren’t ideal. For example, what if you need a set of tuples, or a set of records? Or suppose you want to map records to lists. Belt lets you use any type you want for a key, but with a little bit of setup.
Example: a Belt.Set
of tuples
In my chess tournament app, Coronate, I needed to track which players should avoid being matched with each other. I created a module called AvoidPairs
and made this type:
type t = (string, string)
Each string
is a player’s ID. A value such as ("dale_cooper", "audrey_horne")
means that the players with those ID strings should not be matched. Belt’s Set
data structure was ideal for storing them, because each tuple should be unique. (Possible duplicates would cause bugs elsewhere in the app.) However, the Set
module needs a way to tell which ones are unique or not. Consider that tuples (JS array objects) aren’t considered equal based on their contents. To make this more complex, ("dale_cooper", "audrey_horne")
should be considered exactly the same as ("audrey_horne", "dale_cooper")
. I don’t care about the order as long as they’re paired.
Belt comes with a module called Id
which has helper modules for these situations. In this case, we want to use the Belt.Id.MakeComparable
functor. If “functor” is a new word for you, just think of it as like a module “function” that accepts modules as arguments and returns a new module. Real World OCaml has an in-depth explanation.
Id.MakeComparable
requires a module that has a type t
, and a function cmp
which accepts two t
values and returns an integer (returning 0 means the values are identical).
Here’s the solution I used. Its function cmp
compares the four combinations of IDs and returns the result.
module T = Belt.Id.MakeComparable({
type t = (string, string)
let cmp = ((a, b), (c, d)) => {
let w = compare(a, c)
let x = compare(b, d)
let y = compare(a, d)
let z = compare(b, c)
switch (w, x, y, z) {
/* Sometimes adding them returns 0 even if they're not equivalent.
There might be a better way to pattern-match this, but this works. */
| (1, -1, 1, -1)
| (1, -1, -1, 1) => 1
| (-1, 1, 1, -1)
| (-1, 1, -1, 1) => -1
| (w, x, y, z) => w + x + y + z
}
}
})
May 2021 update: a better way of handling this example is to always ensure the pairs are sorted. You can see how Coronate currently does it here.
The module returned then has to be passed to Belt.Set.make
. You may have read that modules exist on a layer outside of the core language, so you can’t do things like store a module in a value. ReScript provides a way around this with “first-class modules.” It’s a feature that enables us to wrap a module into a value that can then be passed to a function, so we can do this:
let empty = Belt.Set.make(~id=module(T))
And now we have an empty set of tuples. This seems like a lot of trouble up front, but it makes your life easier down the road. For example, I can now do this:
/* I want these players to avoid each other: */
let avoidpairs = Belt.Set.add(empty, ("dale_cooper", "audrey_horne"))
/* Later, I need to check if they're in the set: */
Belt.Set.has(avoidpairs, ("audrey_horne", "dale_cooper"))
/* returns true */
Notice that when I called Set.has
, the tuple I used wasn’t the same as the original? (And even if the values were in the same order, they wouldn’t be ===
equivalent anyway.) Yet it still returned true
, which is the behavior I want. All of that complex logic is already taken care of, and the rest of my code is easier to use as a result.
Example: A Belt.Map
of records
Once you get the hang of this, a lot of concepts become simpler. For example, suppose you have a series of books and you want to map each one to a list of their user reviews.
type book = {
title: string,
author: string,
}
type star =
| One
| Two
| Three
| Four
| Five
type review = {
name: string,
date: float,
text: string,
rating: star,
}
One solution is to give each book a unique ID and then then use Map.String
. But you can also make a map that takes the book
record itself as a key! Simply write a cmp
function that compares books.
module BookCompare = Belt.Id.MakeComparable({
type t = book
let cmp = (a, b) =>
switch compare(a.title, b.title) {
| 0 => compare(a.author, b.author)
| x => x
}
})
let reviews = Belt.Map.make(~id=module(BookCompare))
let book = {title: "Incarnadine", author: "Szybist, Mary"}
let review = {
name: "John",
rating: Five,
text: "Great collection! I liked the poem *Invitation*.",
date: 1568898720165.0,
}
let reviews = Belt.Map.set(reviews, book, list{review})
Ta-da, we used the book as a map key! (Okay, maybe not the physical book itself.) Because we’re comparing the data of books and not an arbitrary ID string, this eliminates bugs such as having duplicate books added. If you do need to distinguish books with the same data, you can just add a new field (such as publisher
or edition
). In our code, if all of the fields match, then it’s the same item.
Again, this may seem like extra work up front, but it will simplify your code in the long run. At some point, you’re going to have to make these comparisons in your code anyway. This ensures that they’re built into your data structures from the beginning.
Top comments (2)
Question. How do you get the data into the ui from
Belt.Map
?That doesnt work, obviously.
Error in console is
I figured that by using
Belt.Map.toArray
i'd get back anArray
i could work with.It looks like the error is coming from this code:
review->React.string
. Thereview
isn’t a string.A few issues are related:
Map.toArray
returns an array of tuples like this:[|(key1, value1), (key2, value2)|]
. If you just want an array of values, then useMap.valuesToArray
to return[|value1, value2|]
.review.text
.I hope that helps!