The ergonomic problem comes from the existential wrapper, but it is only useful when you want to put different kind of shapes in the same container. Otherwise you could stick to jane street design use of first class modules.
What do you think of this ergonomic?
module type SHAPE = sig
module type S = sig
type t
val area : t -> float
val draw : t -> unit
end
(* here for ergonomic reason, it's important to note that this constructor
has only one argument that is a pair *)
type shape = Shape : ('a * (module S with type t = 'a)) -> shape
(* two useful combinators to deal with `shape` type *)
val to_shape : (module S with type t = 'a) -> 'a -> shape
val with_shape :
f:((module S with type t = 'a) -> 'a -> 'b) ->
'a * (module S with type t = 'a) -> 'b
(* here we use the jane street design of first-class modules *)
val foo : (module S with type t = 'a) -> 'a -> int
val bar : (module S with type t = 'a) -> 'a -> unit
end
(* now a signature for a type that implement the Shape interface *)
module type SHAPE_IMP = sig
type t
val area : t -> float
val draw : t -> unit
val make : unit -> t
end
(* an example of code usage with this design *)
module Code (Shape : SHAPE) (Point : SHAPE_IMP) (Rect : SHAPE_IMP) = struct
open Shape
let p1 : Point.t = Point.make ()
let r1 : Rect.t = Rect.make ()
let i : int = foo (module Point) p1
let j : int = foo (module Rect) r1
let () = bar (module Point) p1; bar (module Rect) r1
let () =
(* mix of shapes of different kinds in a list *)
let l : shape list = [
to_shape (module Point) p1;
to_shape (module Rect) r1;
] in
(* that's where it's useful that the Shape constructor has only on argument *)
List.iter (fun (Shape shape) -> with_shape ~f:bar shape) l
end
Edit: add some type annotations in the code usage example to make things clearer.