The shape design problem

IMO, when you have some different kinds of thing that should have the same type (test: does it make sense to have a list with elements of multiple kinds?) then an ADT is probably the right approach:

module Point = struct
  type t = { x : int; y : int }
end

module Rectangle = struct
  type t = { bottom_left : Point.t; top_right : Point.t }
end
module Circle = struct
  type t = { center : Point.t; radius : int }
end

module Shape = struct
  type t =
    | Point of Point.t
    | Rectangle of Rectangle.t
    | Circle of Circle.t
end

You could also have the record definitions inline in the definition of Shape.t.

In cases where you want a new kind of shape, but updating the type is inconvenient (for instance you don’t want to have to update a lot of functions that use it, or you don’t own the type definition), you can do this:

module Shape2 = struct
    type t = Triangle of Point.t * Point.t * Point.t | Shape of Shape.t
end

That doesn’t look super elegant in this case, but in practice I think either it make sense, or it’s a hack and the correct thing is to do the work of updating the original type.