module type ID = sig
type t
val of_string : string -> t
val to_string : t -> string
val (=) : t -> t -> bool
end
module String_id = struct
type t = string
let of_string x = x
let to_string x = x
let (=) = String.(=)
end
module Username : ID = String_id
module Hostname : ID = String_id
But I don’t want to create just IDs with strings, but with other types as well. How would I do that?
I found Id_types.BaseId · ocaml-base-compiler 4.13.1 · OCaml Packages but that doesn’t contain any documentation on how to create these BaseIds,
basically I want something like:
module type ID = sig
type t
val create : 'a -> t
val to_string : t -> string
val (=) : t -> t -> bool
end
module String_id = struct
type t = string
let create x = x
let to_string x = x
let (=) = String.(=)
end
But that doesn’t work val create : 'a -> 'a vs val create: 'a -> string
Or even drop the create from ID and have different implementations for creating. For example a Uuid_ID could have a val create : unit -> t that generates a random Uuid
You need to define a new type for the create argument, as otherwise the 'a means that it should accept anything:
module type ID = sig
type t
type from
val create : from -> t
val to_string : t -> string
val (=) : t -> t -> bool
end
Then you need to be careful not to forget what type is accepted by create:
module Username : ID = String_id
let test = Username.create "id1"
(* Error: This expression has type string but an expression
was expected of type Username.from *)
You can avoid hiding the definition of the type from with a constraint:
module Username : ID with type from = string = String_id
module Genid : ID with type from = unit = Uuid_ID
@art-w’s answer is the best one for the question you’re asking, but I’m interested in your reason for generalising over constructors in the first place. Do you want:
to have a common API for a range of implementations for the sake of providing consistency to users (who otherwise know which specific ID implementation they want);
to have a common API so that it can be generalised over? (e.g. by having a functor that consumes ID implementations and knows how to build them.)
… or something else? My experience is that (2) generally doesn’t scale: constructors end up being too tied to their particular implementations to make for nice abstractions. I think (1) can work well, but for public module types I’d still recommend defining a separate, smaller module type that excludes create so that consumers don’t inherit the choice.
module Id : sig
module type S = sig
type t
val of_string : string -> t
val to_string : t -> string
val (=) : t -> t -> bool
end
module type With_create = sig
include S
type from
val create : from -> t
end
end
Consumers who want to generalise over Id.S can then do so while still consuming ID generators that cannot conform to the create types shared by your pre-provided implementations. This is unlikely to be an issue for you, but it regularly is for me and so it was on my mind