The problem with classes (in any language) is that they mix and confuse implementation with abstraction. Basically, when you use the inheritance relation you’re invoking two orthogonal things at once, the inheritance of implementation (aka subclassing) and the refinement of type/interface (aka subtyping).
These are completely different things, that should be addressed differently. The implementation inheritance enables code reuse, and perhaps is the worst possible way to provide the code use, hence it is not even recommended in Java/C++ and other OO-focused languages. Fortunately, OCaml provides lots of options for code reuse, that includes first class functions, modules, functors, all powered with parametric polymorphism with principal and automatic type inference.
The refinement of a type is what you’re actually want to model (as code reuse is the implementation detail). When people say that we think in terms of objects, like that a bird is an animal, it doesn’t really mean that we think that birds inherit the implementation of animals and that there is a locomotion
method implemented in all animals which is overridden by different implementations. No, we think in terms of phenomena which we observe and generalize into concepts. So that we can see that different objects and processes may exhibit common behaviors, so we have an opportunity to generalize them by creating an abstraction. That brings us to the next level of understanding on which we think already in terms of abstractions, which we can generalize even further, and so on until we reach the level of category theory, where we are no longer constrained with understanding .
This modus of operandi is modeled in OCaml extremely well, as we have signatures to model different concepts, e.g., for example, we may observe that all animals are concrete objects which come in and out of existence, and that each Animal is an individual which we can somehow identify, so we model it with the signature that defines the set of operations that we believe, in domain of discourse of our model, will abstract an animal correctly:
module type Animal = sig
type t
val create : id -> t
val id : t -> id
val age : t -> int
val is_alive : t -> bool
end
We can see, als, that some Animals are flying. In our model, we may decide that is worthwhile to distinguish them from other animals, e.g.,
module type Flying = sig
include Animal
val fly : position -> unit world
end
So, we can see that Flying
is an Animal
, no matter the implementation or other details - we can always expect that any Flying Animal will exhibit the behavior of an animal (wrt to our model, again).
Once you will start implementing your hierarchies you will be happily surprised that OCaml will provide you with means to reuse the implementation. Like you may actually decide to reuse the Animal data type for all your animals (indeed, there is no need to reimplement the animal bookeeping in each module implementation). The good thing is that your choice of implementation details wouldn’t affect your hierarchies as they are completely orthogonal.
As a final note, OCaml uses the word type
for whatever is called datatype
in SML, data
in Haskell, struct
or class
in C/C++ or Java, for a thing that describes a data type, i.e., the concrete implementation. There is nothing wrong with it, except that it brings confusion between types and abstractions, which usually deemed equal. In OCaml, abstractions are modeled with module types. So type animal
is a concrete implementation (which could be abstracted by its signature, where the signature is the abstraction). Hence, when you have a UML diagram that models some entities, then those entities should map to module types, and operations, along with their constraints (in OCL) should map to functions and their types. So classes in UML are module types in OCaml.