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.