typesmodulefunctorrescript

Is a simple way to use `Belt.Set` (and others) fixing its type?


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?


Solution

  • 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.