The shape design problem

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.