I'm creating a functor for opaque identifiers to allow the type system help catching issues where you're supposed to have an identifier of A, but you're being given an identifier of B.
I've already have the core of it working, but I want some standard (Belt) modules to be nicely implemented to the new types.
This is what I have:
module MakeOpaqueIdentifier = () => {
type t
%%private(external fromString: string => t = "%identity")
external toString: t => string = "%identity"
let make: unit => t = () => UUID7.make()->fromString
let null: t = ""->fromString
let zero: t = "00000000-0000-0000-0000-000000000000"->fromString
module Comparator = Belt.Id.MakeComparable({
type t = t
let cmp = Pervasives.compare
})
module Hasher = Belt.Id.MakeHashable({
type t = t
let hash = x => x->Hashtbl.hash
let eq = (a, b) => Pervasives.compare(a, b) == 0
})
}
But I want to easily create Belt.Set
of those identifiers. Since the ids are actually strings I would like to use Belt.Set.String
but expose all the interface of Belt.Set
with type t = Belt.Set.t<t, Hasher.identity>
. Initially I tried the following:
module MakeOpaqueIdentitier = {
/// same code as before
type set = Belt.Set.t<t, Hasher.identity>
module Set: module type of Belt.Set with type t = set = {
include Belt.Set
}
}
But that fails with:
38 ┆
39 ┆ type set = Belt.Set.t<t, Hasher.identity>
40 ┆ module Set: module type of Belt.Set with type t = set = {
41 ┆ include Belt.Set
42 ┆ }
In this `with' constraint, the new definition of t
does not match its original definition in the constrained signature:
Type declarations do not match:
type rec t = set
is not included in
type rec t<'value, 'identity> = Belt_Set.t<'value, 'identity>
...
They have different arities.
Is there a way to accomplish this?
This unfortunately isn't possible, because type variables can't be substituted on a module level.
Consider a module signature like this:
module type S = {
type t<'a>
let fromFloat: float => t<float>
let getString: t<string> => string
}
If you substitute t<'a>
with t<int>
here, for example, what would you do with the signatures of fromFloat
and getString
?
The way to make module level variables that can be substituted is to declare the types directly in the module:
module type S = {
type value
type t
let make: value => t
let get: t => value
}
module type T = S with type value = string
That of course removes "value-level" polymorphism, making it impossible to have specialized functions like fromFloat
and getString
among the fully generic functions. That's one of the trade-offs you would have to make.