What is the right way to put type declarations in OCaml signatures

47 Views Asked by At

I have written the following two files, and although it works, it feels odd that the code would duplicate these long type declarations. On the other hand, if I remove either module type declaration then the code no longer compiles.

Is there a more proper way to write this kind of thing? Or is it correct to duplicate these type declarations in the .mli and .ml files?

set.mli

module type Element = sig
  type t
  val create : 'a -> t
  val compare : t -> t -> int
  val to_string : t -> string
end

module type Set = sig
  type t
  val empty : unit -> t
end

module Make : functor (M : Element) -> Set with type t = M.t list

set.ml

module type Element = sig
  type t
  val create : 'a -> t
  val compare : t -> t -> int
  val to_string : t -> string
end

module type Set = sig
  type t
  val empty : unit -> t
end

module Make (M:Element) = struct
  type t = M.t list
  let emtpy () = []
end
3

There are 3 best solutions below

3
Chris On BEST ANSWER

A good reference for this would be to review how the standard library is implemented. It's doubtful you're going to find a "better" way to do things than they do.

I you consider set.mli and set.ml you'll see that they are specified very similarly to your code.

What is curious in your code is why empty is implemented as a function, rather than simply a value. Note that you have a typo with emtpy in your second to last line.

0
glennsl On

One way to reduce duplication is to use what's called the intf trick. By moving type definitions into a separate set_intf.ml file, you can include that in both set.ml and set.mli:

set_intf.ml

module type Element = sig
  type t
  val create : 'a -> t
  val compare : t -> t -> int
  val to_string : t -> string
end

module type Set = sig
  type t
  val empty : unit -> t
end

set.mli

include Set_intf

module Make : functor (M : Element) -> Set with type t = M.t list

set.ml

include Set_intf

module Make (M:Element) = struct
  type t = M.t list
  let empty () = []
end

This indirection makes the code a little less readable, but can be worth it if a lot of duplication is needed.

0
ivg On

A common approach (as utilized by the Base and Core libraries) is to use an ml file that has only the type definitions, e.g.,

(* file set_intf.ml *)
module type Element = sig
  type t
  val create : 'a -> t
  val compare : t -> t -> int
  val to_string : t -> string
end

module type S = sig
  type t
  val empty : unit -> t
end

module type Set = sig
  module Make : functor (M : Element) -> S with type t = M.t list
end

and then

(* file set.mli *)
include Set_intf.Set (** @inline *)

and finally

(* file set.ml *)
include Set_intf

module Make(M : Element) = struct
  type t = M.t list
  let empty () = []
end

For such small interfaces, it might be an overkill and even hamper readability, but when you have huge interfaces it might become inevitable.

You shouldn't, however, be afraid of repetitions in signatures. Albeit a little bit tedious, they are not code repetitions, as types are not code. You can't have bugs in it and any mismatch will be immediately pinpointed to you by the compiler.