Something additional to keep in mind is that OCaml tends to use functors much more than ReScript does. The reason is that the generated code is difficult to optimize, which is especially a problem when compiling to JavaScript. The OCaml stdlib Map
, for example, uses a functor to generate all of the Map
functions, whereas the Belt.Map
does not.
As to why functors are necessary for maps, sets, etc, it’s related to type safety and the fact that functors are one of the few ways we can dynamically create new types.
To use Belt.Map
as the example, you’ll notice that its type is defined like this: Belt.Map.t<'key, 'value, 'identity>
. The key
and value
parameters are probably obvious since they correspond to how we normally think about maps (in the example posted above, key = int
and value = string
).
The problem is that it’s actually possible to create different maps that have the same key and value types but are implemented differently. Internally, Belt.Map
uses the cmp
function to organize its keys. From a type system perspective, these following functions are identical even though they do different things:
let cmp1 = (a, b) => compare(a, b)
let cmp2 = (a, b) => compare(b, a)
We need a way to express that different implementation in the type system, which is what the identity
type parameter does.
The MakeComparable
functor creates a unique identity
type to solve the problem for us. What happens if you try to merge two maps that sort their keys completely differently? With Belt’s implementation, you receive a type error.
Example code
module IntCmp1 = Belt.Id.MakeComparable({
type t = int
let cmp = (a: t, b: t) => compare(a, b)
})
module IntCmp2 = Belt.Id.MakeComparable({
type t = int
let cmp = (a: t, b: t) => compare(b, a) // This is different!
})
let m1 = Belt.Map.fromArray(~id=module(IntCmp1), [(1, "a"), (2, "b")])
let m2 = Belt.Map.fromArray(~id=module(IntCmp2), [(3, "c"), (4, "d")])
let m3 = Belt.Map.merge(m1, m2 /* <- This is an error! */, (_k, v1, v2) =>
switch (v1, v2) {
| (x, None) | (_, x) => x
}
)
Triggers this error message:
This has type:
Belt.Map.t<IntCmp2.t, string, IntCmp2.identity> (defined as
Belt_Map.t<IntCmp2.t, string, IntCmp2.identity>)
Somewhere wanted:
Belt.Map.t<IntCmp1.t, string, IntCmp1.identity> (defined as
Belt_Map.t<IntCmp1.t, string, IntCmp1.identity>)
The incompatible parts:
IntCmp2.identity vs IntCmp1.identity
All of this is why passing cmp
as a simple callback function would not be sufficient. We need a way to “sign” the function with a unique type, which is where the functor system comes into play.
The OCaml stdlib Map
works differently, but is ultimately based on the same idea. When you use the OCaml Map.Make
functor, it also uses its input to create a new type. Different maps based on different input modules (and different compare
functions) are incompatible.