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:
If you substitute
t<'a>witht<int>here, for example, what would you do with the signatures offromFloatandgetString?The way to make module level variables that can be substituted is to declare the types directly in the module:
That of course removes "value-level" polymorphism, making it impossible to have specialized functions like
fromFloatandgetStringamong the fully generic functions. That's one of the trade-offs you would have to make.