OCaml provides several mechanisms for generalization and abstraction. Sometimes it is not obvious which one to choose. In that case, I usually suggest to use the most lightweight mechanism available, in other words, do not use a mechanism that gives you more than you need (unless you anticipate that in the future you will need more).
So, you have the following choices:
- Functor
- Function
- Class
- Object
Functor
Functors provide the most powerful generalization mechanism. They use module types to specify abstractions and modules as implementations. This comes with a cost of course. Many people find, that functors are syntactically heavy.
module type Coordinates = sig
val transform : (int * int) -> (int * int)
end
module Canvas(Coord : Coordinates) = struct
let draw obj =
let x,y = Coord.transform obj.coords in
...
end
And here is the usage:
module Canvas = Canvas(Minimap)
Canvas.draw world
Function
A module is just a compound data structure, that can contain functions, types, and other modules. When it is compiled, the types and other ephemeral declarations are removed, so the module representation is actually the same as the representation of a record. A functor is actually compiled as a function, parameterized by a record. It looks like, that we're not trying to parametrize our "set of functions" with any types or modules, so we can just use Occam razor, and use a record directly, without module encapsulation, e.g.,
type canvas = {
draw : object -> unit;
...
}
let canvas xform = {
draw = (fun obj -> ... );
...
}
And usage:
let canvas = canvas xform
canvas.draw world
Class
Classes, like modules, also encapsulate several fields into one entity. They can contain methods (i.e., functions) and instance variables. They can't contain types, however. The main difference between classes and modules is that class functions bind to each other not at the compile time, but at runtime (so called late binding). It means, that whenever you call a class method, you're actually not calling a concrete function, but you're invoking a whatever function is currently written in the slot associated with the invoked method. Basically, that means, that a class is a set of mutually recursive functions, that can be mutated during runtime. In other words, the mutual recursion is open, i.e., a user of your class can change the implementation of any method, via the inheritance. Basically, for you, it means, that you can't trust your own methods and that the type of your object is not fixed, but is open. All these together makes reasoning about classes very hard, so if you don't need open recursing, then I would suggest keeping away from classes. To protect the idea of classes, I would like to notice that they are good for implementing AST visitors, interpreters, and other algorithms that work with data structures that are inherently recursive.
So here is the example:
class canvas xform = object
method draw : obj -> unit =
let x,y = xform obj.coords in
...
end
Usage
let canvas = new canvas xform
canvas#draw world
Objects
When you define a class, you're actually defining a blueprint, that describes how to create objects of that type. But in OCaml, it is not required for an object to be derived from a base class, or to be created using a class. For an object to be recognized as an instance of a class it must provide all methods, that are specified in this class (with matching method types of course). In other words, OCaml type system is not nominal, but structural -- if an object has all the methods, and all the types of methods are included in the required type, then the object is of that type. So, when you are expressing something as a class, then you're actually anticipating that someone will inherit your class, and change methods implementation. This puts an extra restriction on your implementation, and types of your methods. So, if you're not anticipating the inheritance, it is better just to use an object (however, if you're not anticipating an inheritance, then why to use classes at all). Here is the syntax example:
let canvas xform = object
method draw : obj -> unit =
let x,y = xform obj.coords in
...
end
Yep, just substitute class
with let
, and you will create a value, instead of a class. But this substitution will give you a big advantage in your quarrels with the type checker. Here comes the usage example:
let canvas = canvas xform
canvas#draw world
Summary
There are actually other mechanisms, that are more obscure, like using first class modules. But following the Occam razor principle, we will cut them of the posting. Of all the mechanisms, that I've shown so far, I would suggest you either functors or records. If you think, that at some point in time, you will abstract the coordinates representation, then I would suggest using functors. If you want to keep it simple and concrete, then use records. Don't use classes unless your functions are mutually recursive, and you want your users to hook into your methods.