kuniga.me > NP-Incompleteness > OCaml Modules
18 Jul 2016
One prevalent syntax in some OCaml code I’ve encountered is about modules, so I decided to study them a little bit to be able to better understand the code I’ve been reading.
This post is based on Chapters 4 and 9 from Real World OCaml [1, 2], which we started in the last post.
By default, a file defines a module. Module names are derived from filenames and is always capitalized (even if the filename is not). Let’s define a toy example:
myModule.ml
:
main.ml
:
We can now compile using the built-in ocamlc
(make sure to follow the setup here):
Note that the order is important. Since main.ml
depends on myModule.ml
, it has to be included first. In case the files do not live in the same directory, we can use the -I
option. For example, if myModule
was in another_dir
, we could compile using:
A module has two parts: the definition and the signature. A module defined by a file filename.ml
can be constrained by a signature in a file called filename.mli
. The definition (mli
) should be included in the compiling step and appear before the definition. For example, we could have
myModule.mli
:
and compile using:
Submodules
It’s possible to define multiple modules inside a file through submodules. We can add this to myModule.ml
:
and in main.ml
:
Note that we still need to provide the module name defined by the filename. The first part of the definition is the type signature of the module and the second the definition. The general syntax is:
Alternatively, we can separate the type definition from the implementation. In that case, we create a separate file with extension .mli
containing the interface
myModule.mli
:
and in myModule.ml
:
In general, the syntax for the signature is:
and for the module definition is:
Modules are made available when linking during compile time, but if we want to use a function from a module, we still need to qualify it. We can alternatively include them explicitly to avoid qualifying module names (similar to use namespace in C++).
Instead of:
We can also invoke open
inline:
or alias the module to a shorter name with the let module
construct:
Functors are functions that transform modules, so it’s a function from a module to a module. A basic example is provided in [2]. First we define a toy signature signature:
Then, we define a functor, which we call Increment
:
What tells us this is a function is the extra parameter the module takes (M: X_int)
. In here, M
is the name we give to the module and X_int
is its interface. Since the interface tells us M
has the x
value, we can access it within our function. Note that Increment
acts like a function, taking M
(module) as parameter and returning another module, defined by the struct
block. In this case, the type signature is different because the returned module has y
instead of x
. We can force the returned type of a function by adding a constraint:
Now, if we try to use y
, we can a compilation error. To fix it, we just change y
to x
.
Functors cannot be used by themselves. They’re useful for creating modules out of existing modules. For an example, imagine we have a module implementing X_int
:
We can create a new module Four
, by transforming our Three
module:
In [2], the authors provide an example of an MakeInterval
module, in which there’s a dependent type. First it creates a Comparable
signature:
to make it shorter (and less “real world”), I’ve created a simpler version here, MakeElement
:
we can then create a module:
The above works because Int
satisfies the constraint defined by the Comparable
module signature. The authors make a point here that sticking to standard conventions can improve reuse such as cases like this. We can finally use
The authors argue that the IntElement
exposes implementation details because Element
is “exposed”:
One solution is to constrain the return type of the functor and not expose Element
in the signature. The signature would look like:
and the functor would look like:
The problem is the type element is not bound to anything, so we have to explicitly do it when defining the functor. The construct is the following
now MakeElement
return a module with interface ElementInterface
which doesn’t expose Element
.
One problem with the approach above is the redundant binding of type element
. One slightly different syntax removes that requirement:
We basically changed from =
to :=
, which is called destructive substitution.
It’s possible to “extend” more than one module signature when creating a new one. For example:
Modules seems a very powerful concept in Ocaml. Besides organizing files, modules can act as functions and can model concepts from Object Oriented Programming like classes and interfaces.
I’ve been following a different study strategy while learning Ocaml. I try to read some real world code and when I get stuck understand a syntax or a pattern, I can search for them in a book. This makes it more interesting than reading a book cover to cover.