Modules
Basic usage
In OCaml, every piece of code is wrapped into a module. Optionally, a module itself can be a submodule of another module, pretty much like directories in a file system-but we don't do this very often.
When you write a program let's say using two files amodule.ml
and
bmodule.ml
, each of these files automatically defines a module named
Amodule
and a module named Bmodule
that provide whatever you put
into the files.
Here is the code that we have in our file amodule.ml
:
let hello () = print_endline "Hello"
And here is what we have in bmodule.ml
:
Amodule.hello ()
Usually files are compiled one by one, let's do it:
ocamlopt -c amodule.ml
ocamlopt -c bmodule.ml
ocamlopt -o hello amodule.cmx bmodule.cmx
Now we have a wonderful executable that prints "Hello". As you can see, if you want to access anything from a given module, use the name of the module (always starting with a capital) followed by a dot and the thing that you want to use. It may be a value, a type constructor, or anything else that a given module can provide.
Libraries, starting with the standard library, provide collections of
modules. for example,
List.iter
designates the iter
function from
the List
module.
OK, if you are using a given module heavily, you may want to make its
contents directly accessible. For this, we use the open
directive. In
our example, bmodule.ml
could have been written:
open Amodule;;
hello ();;
As a side note, people tend to avoid the ugly ";;", so it more common to write it like:
open Amodule
let () =
hello ()
Anyway, using open
or not is a matter of personal taste. Some modules
provide names that are used in many other modules. This is the case of
the List
module for instance. Usually we don't do open List
. Other
modules like
Printf
provide names that are normally not subject to
conflicts, such as printf
. In order to avoid writing Printf.printf
all over the place, it often makes sense to place one open Printf
at
the beginning of the file.
There is a short example illustrating (in the interactive toplevel) what we just mentioned:
# open Printf
let my_data = [ "a"; "beautiful"; "day" ]
let () = List.iter (fun s -> printf "%s\n" s) my_data;;
a
beautiful
day
val my_data : string list = ["a"; "beautiful"; "day"]
Interfaces and signatures
A module can provide a certain number of things (functions, types, submodules, ...) to the rest of the program that is using it. If nothing special is done, everything which is defined in a module will be accessible from outside. That's often fine in small personal programs, but there are many situations where it is better that a module only provides what it is meant to provide, not any of the auxiliary functions and types that are used internally.
For this we have to define a module interface, which will act as a mask
over the module's implementation. Just like a module derives from a .ml
file, the corresponding module interface or signature derives from a
.mli file. It contains a list of values with their type, and more. Let's
rewrite our amodule.ml
file:
# let message = "Hello"
let hello () = print_endline message;;
val message : string = "Hello"
val hello : unit -> unit = <fun>
As it is, Amodule
has the following interface:
val message : string
val hello : unit -> unit
Let's assume that accessing the message
value directly is none of the
others modules' business. We want to hide it by defining a restricted
interface. This is our amodule.mli
file:
val hello : unit -> unit
(** Displays a greeting message. *)
(note that it is a good habit to document .mli files, using the format supported by ocamldoc)
.mli files must be compiled just before the matching .ml files. They are
compiled using ocamlc
, even if .ml files are compiled to native code
using ocamlopt
:
ocamlc -c amodule.mli
ocamlopt -c amodule.ml
...
Abstract types
What about type definitions? We saw that values such as functions can be exported by placing their name and their type in a .mli file, e.g.
val hello : unit -> unit
But modules often define new types. Let's define a simple record type that would represent a date:
type date = { day : int; month : int; year : int }
There are not two, but four options when it comes to writing the .mli file:
- The type is completely omitted from the signature.
- The type definition is copy-pasted into the signature.
- The type is made abstract: only its name is given.
- The record fields are made read-only:
type date = private { ... }
In case 3, it would be the following code:
type date
Now, users of the module can manipulate objects of type date
, but they
can't access the record fields directly. They must use the functions
that the module provides. Let's assume the module provides three
functions, one for creating a date, one for computing the difference
between two dates, and one that returns the date in years:
type date
val create : ?days:int -> ?months:int -> ?years:int -> unit -> date
val sub : date -> date -> date
val years : date -> float
The point is that only create
and sub
can be used to create date
records. Therefore, it is not possible for the user of the module to
create ill-formed records. Actually, our implementation uses a record,
but we could change it and be sure that it will not break any code that
relies on this module! This makes a lot of sense in a library since
subsequent versions of the same library can continue to expose the same
interface, while internally changing the implementation, including data
structures.
Submodules
Submodule implementation
We saw that one example.ml
file results automatically in one module
implementation named Example
. Its module signature is automatically
derived and is the broadest possible, or can be restricted by writing an
example.mli
file.
That said, a given module can also be defined explicitly from within a
file. That makes it a submodule of the current module. Let's consider
this example.ml
file:
module Hello = struct
let message = "Hello"
let hello () = print_endline message
end
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
Hello.hello ();
goodbye ()
From another file, it is clear that we now have two levels of modules. We can write:
let () =
Example.Hello.hello ();
Example.goodbye ()
Submodule interface
We can also restrict the interface of a given submodule. It is called a
module type. Let's do it in our example.ml
file:
module Hello : sig
val hello : unit -> unit
end =
struct
let message = "Hello"
let hello () = print_endline message
end
(* At this point, Hello.message is not accessible anymore. *)
let goodbye () = print_endline "Goodbye"
let hello_goodbye () =
Hello.hello ();
goodbye ()
The definition of the Hello
module above is the equivalent of a
hello.mli
/hello.ml
pair of files. Writing all of that in one block
of code is not elegant, so in general we prefer to define the module
signature separately:
module type Hello_type = sig
val hello : unit -> unit
end
module Hello : Hello_type = struct
...
end
Hello_type
is a named module type, and can be reused to define other
module interfaces.
Although having submodules may be useful in some cases, their real utility becomes apparent with functors. This is the next section.
Functors
Functors are probably one of the most complex features of OCaml, but you don't have to use them extensively to be a successful OCaml programmer. Actually, you may never have to define a functor yourself, but you will surely encounter them in the standard library. They are the only way of using the Set and Map modules, but using them is not so difficult.
What is a functor and why do we need them?
A functor is a module that is parametrized by another module, just like a function is a value which is parametrized by other values, the arguments.
Basically, it allows to parametrize a type by a value, which is not possible directly in OCaml. For example, we can define a functor that takes an int n and returns a collection of array operations that work exclusively on arrays of length n. If by mistake the programmer passes a regular array to one of those functions, it will result in a compilation error. If we were not using this functor but the standard array type, the compiler would not be able to detect the error, and we would get a runtime error at some undetermined date in the future, which is much worse.
How to use an existing functor?
The standard library defines a Set
module, which provides a Make
functor. This functor takes one argument, which is a module that
provides (at least) two things: the type of elements, given as t
and
the comparison function given as compare
. The point of the functor is
to ensure that the same comparison function will always be used, even if
the programmer makes a mistake.
For example, if we want to use sets of ints, we would do this:
# module Int_set = Set.Make (struct
type t = int
let compare = compare
end);;
module Int_set :
sig
type elt = int
type t
val empty : t
val is_empty : t -> bool
val mem : elt -> t -> bool
val add : elt -> t -> t
val singleton : elt -> t
val remove : elt -> t -> t
val union : t -> t -> t
val inter : t -> t -> t
val disjoint : t -> t -> bool
val diff : t -> t -> t
val compare : t -> t -> int
val equal : t -> t -> bool
val subset : t -> t -> bool
val iter : (elt -> unit) -> t -> unit
val map : (elt -> elt) -> t -> t
val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
val for_all : (elt -> bool) -> t -> bool
val exists : (elt -> bool) -> t -> bool
val filter : (elt -> bool) -> t -> t
val partition : (elt -> bool) -> t -> t * t
val cardinal : t -> int
val elements : t -> elt list
val min_elt : t -> elt
val min_elt_opt : t -> elt option
val max_elt : t -> elt
val max_elt_opt : t -> elt option
val choose : t -> elt
val choose_opt : t -> elt option
val split : elt -> t -> t * bool * t
val find : elt -> t -> elt
val find_opt : elt -> t -> elt option
val find_first : (elt -> bool) -> t -> elt
val find_first_opt : (elt -> bool) -> t -> elt option
val find_last : (elt -> bool) -> t -> elt
val find_last_opt : (elt -> bool) -> t -> elt option
val of_list : elt list -> t
val to_seq_from : elt -> t -> elt Seq.t
val to_seq : t -> elt Seq.t
val add_seq : elt Seq.t -> t -> t
val of_seq : elt Seq.t -> t
end
For sets of strings, it is even easier because the standard library
provides a String
module with a type t
and a function compare
. If
you were following carefully, by now you must have guessed how to create
a module for the manipulation of sets of strings:
# module String_set = Set.Make (String);;
module String_set :
sig
type elt = String.t
type t = Set.Make(String).t
val empty : t
val is_empty : t -> bool
val mem : elt -> t -> bool
val add : elt -> t -> t
val singleton : elt -> t
val remove : elt -> t -> t
val union : t -> t -> t
val inter : t -> t -> t
val disjoint : t -> t -> bool
val diff : t -> t -> t
val compare : t -> t -> int
val equal : t -> t -> bool
val subset : t -> t -> bool
val iter : (elt -> unit) -> t -> unit
val map : (elt -> elt) -> t -> t
val fold : (elt -> 'a -> 'a) -> t -> 'a -> 'a
val for_all : (elt -> bool) -> t -> bool
val exists : (elt -> bool) -> t -> bool
val filter : (elt -> bool) -> t -> t
val partition : (elt -> bool) -> t -> t * t
val cardinal : t -> int
val elements : t -> elt list
val min_elt : t -> elt
val min_elt_opt : t -> elt option
val max_elt : t -> elt
val max_elt_opt : t -> elt option
val choose : t -> elt
val choose_opt : t -> elt option
val split : elt -> t -> t * bool * t
val find : elt -> t -> elt
val find_opt : elt -> t -> elt option
val find_first : (elt -> bool) -> t -> elt
val find_first_opt : (elt -> bool) -> t -> elt option
val find_last : (elt -> bool) -> t -> elt
val find_last_opt : (elt -> bool) -> t -> elt option
val of_list : elt list -> t
val to_seq_from : elt -> t -> elt Seq.t
val to_seq : t -> elt Seq.t
val add_seq : elt Seq.t -> t -> t
val of_seq : elt Seq.t -> t
end
(the parentheses are necessary)
How to define a functor?
A functor with one argument can be defined like this:
module F (X : X_type) = struct
...
end
where X
is the module that will be passed as argument, and X_type
is
its signature, which is mandatory.
The signature of the returned module itself can be constrained, using this syntax:
module F (X : X_type) : Y_type =
struct
...
end
or by specifying this in the .mli file:
module F (X : X_type) : Y_type
Overall, the syntax of functors is hard to grasp. The best may be to
look at the source files
set.ml
or
map.ml
of the standard library.
Final remark: functors are made to help programmers write correct programs, not to improve performance. There is even a runtime penalty, unless you use a defunctorizer such as ocamldefun, which requires access to the source code of the functor.
Practical manipulation of modules
Displaying the interface of a module
You can use the ocaml
toplevel to visualize the contents of an existing
module, such as List
:
# #show List;;
module List = List
module List :
sig
type 'a t = 'a list = [] | (::) of 'a * 'a list
val length : 'a list -> int
val compare_lengths : 'a list -> 'b list -> int
val compare_length_with : 'a list -> int -> int
val cons : 'a -> 'a list -> 'a list
val hd : 'a list -> 'a
val tl : 'a list -> 'a list
val nth : 'a list -> int -> 'a
val nth_opt : 'a list -> int -> 'a option
val rev : 'a list -> 'a list
val init : int -> (int -> 'a) -> 'a list
val append : 'a list -> 'a list -> 'a list
val rev_append : 'a list -> 'a list -> 'a list
val concat : 'a list list -> 'a list
val flatten : 'a list list -> 'a list
val iter : ('a -> unit) -> 'a list -> unit
val iteri : (int -> 'a -> unit) -> 'a list -> unit
val map : ('a -> 'b) -> 'a list -> 'b list
val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list
val rev_map : ('a -> 'b) -> 'a list -> 'b list
val filter_map : ('a -> 'b option) -> 'a list -> 'b list
val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b
val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a
val fold_right2 :
('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c
val for_all : ('a -> bool) -> 'a list -> bool
val exists : ('a -> bool) -> 'a list -> bool
val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
val mem : 'a -> 'a list -> bool
val memq : 'a -> 'a list -> bool
val find : ('a -> bool) -> 'a list -> 'a
val find_opt : ('a -> bool) -> 'a list -> 'a option
val filter : ('a -> bool) -> 'a list -> 'a list
val find_all : ('a -> bool) -> 'a list -> 'a list
val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
val assoc : 'a -> ('a * 'b) list -> 'b
val assoc_opt : 'a -> ('a * 'b) list -> 'b option
val assq : 'a -> ('a * 'b) list -> 'b
val assq_opt : 'a -> ('a * 'b) list -> 'b option
val mem_assoc : 'a -> ('a * 'b) list -> bool
val mem_assq : 'a -> ('a * 'b) list -> bool
val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list
val split : ('a * 'b) list -> 'a list * 'b list
val combine : 'a list -> 'b list -> ('a * 'b) list
val sort : ('a -> 'a -> int) -> 'a list -> 'a list
val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list
val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list
val sort_uniq : ('a -> 'a -> int) -> 'a list -> 'a list
val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
val to_seq : 'a list -> 'a Seq.t
val of_seq : 'a Seq.t -> 'a list
end
Otherwise, there is online documentation for most libraries or you can
use ocamlbrowser
which comes with labltk (Tk graphical user interface
for OCaml).
Module inclusion
Let's say we feel that a function is missing from the standard List
module, but we really want it as if it were part of it. In an
extensions.ml
file, we can achieve this effect by using the include
directive:
# module List = struct
include List
let rec optmap f = function
| [] -> []
| hd :: tl ->
match f hd with
| None -> optmap f tl
| Some x -> x :: optmap f tl
end;;
module List :
sig
type 'a t = 'a list = [] | (::) of 'a * 'a list
val length : 'a list -> int
val compare_lengths : 'a list -> 'b list -> int
val compare_length_with : 'a list -> int -> int
val cons : 'a -> 'a list -> 'a list
val hd : 'a list -> 'a
val tl : 'a list -> 'a list
val nth : 'a list -> int -> 'a
val nth_opt : 'a list -> int -> 'a option
val rev : 'a list -> 'a list
val init : int -> (int -> 'a) -> 'a list
val append : 'a list -> 'a list -> 'a list
val rev_append : 'a list -> 'a list -> 'a list
val concat : 'a list list -> 'a list
val flatten : 'a list list -> 'a list
val iter : ('a -> unit) -> 'a list -> unit
val iteri : (int -> 'a -> unit) -> 'a list -> unit
val map : ('a -> 'b) -> 'a list -> 'b list
val mapi : (int -> 'a -> 'b) -> 'a list -> 'b list
val rev_map : ('a -> 'b) -> 'a list -> 'b list
val filter_map : ('a -> 'b option) -> 'a list -> 'b list
val fold_left : ('a -> 'b -> 'a) -> 'a -> 'b list -> 'a
val fold_right : ('a -> 'b -> 'b) -> 'a list -> 'b -> 'b
val iter2 : ('a -> 'b -> unit) -> 'a list -> 'b list -> unit
val map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
val rev_map2 : ('a -> 'b -> 'c) -> 'a list -> 'b list -> 'c list
val fold_left2 : ('a -> 'b -> 'c -> 'a) -> 'a -> 'b list -> 'c list -> 'a
val fold_right2 :
('a -> 'b -> 'c -> 'c) -> 'a list -> 'b list -> 'c -> 'c
val for_all : ('a -> bool) -> 'a list -> bool
val exists : ('a -> bool) -> 'a list -> bool
val for_all2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
val exists2 : ('a -> 'b -> bool) -> 'a list -> 'b list -> bool
val mem : 'a -> 'a list -> bool
val memq : 'a -> 'a list -> bool
val find : ('a -> bool) -> 'a list -> 'a
val find_opt : ('a -> bool) -> 'a list -> 'a option
val filter : ('a -> bool) -> 'a list -> 'a list
val find_all : ('a -> bool) -> 'a list -> 'a list
val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
val assoc : 'a -> ('a * 'b) list -> 'b
val assoc_opt : 'a -> ('a * 'b) list -> 'b option
val assq : 'a -> ('a * 'b) list -> 'b
val assq_opt : 'a -> ('a * 'b) list -> 'b option
val mem_assoc : 'a -> ('a * 'b) list -> bool
val mem_assq : 'a -> ('a * 'b) list -> bool
val remove_assoc : 'a -> ('a * 'b) list -> ('a * 'b) list
val remove_assq : 'a -> ('a * 'b) list -> ('a * 'b) list
val split : ('a * 'b) list -> 'a list * 'b list
val combine : 'a list -> 'b list -> ('a * 'b) list
val sort : ('a -> 'a -> int) -> 'a list -> 'a list
val stable_sort : ('a -> 'a -> int) -> 'a list -> 'a list
val fast_sort : ('a -> 'a -> int) -> 'a list -> 'a list
val sort_uniq : ('a -> 'a -> int) -> 'a list -> 'a list
val merge : ('a -> 'a -> int) -> 'a list -> 'a list -> 'a list
val to_seq : 'a list -> 'a Seq.t
val of_seq : 'a Seq.t -> 'a list
val optmap : ('a -> 'b option) -> 'a t -> 'b t
end
It creates a module Extensions.List
that has everything the standard
List
module has, plus a new optmap
function. From another file, all
we have to do to override the default List
module is open Extensions
at the beginning of the .ml file:
open Extensions
...
List.optmap ...