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 ()
We can compile the files in one command:
ocamlopt -o hello amodule.ml bmodule.ml
Or, as a build system might do, one by one:
ocamlopt -c amodule.ml
ocamlopt -c bmodule.ml
ocamlopt -o hello amodule.cmx bmodule.cmx
Now we have an 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.
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;;
let () = hello ();;
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:
# open Printf;;
# let data = ["a"; "beautiful"; "day"];;
val data : string list = ["a"; "beautiful"; "day"]
# let () = List.iter (printf "%s\n") data;;
a
beautiful
day
There are also local open
s:
# let map_3d_matrix f m =
let open Array in
map (map (map f)) m;;
val map_3d_matrix :
('a -> 'b) -> 'a array array array -> 'b array array array = <fun>
# let map_3d_matrix' f =
Array.(map (map (map f)));;
val map_3d_matrix' :
('a -> 'b) -> 'a array array array -> 'b array array array = <fun>
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 the 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 an .mli file. It
contains a list of values with their type. 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 the double asterisk at the beginning of the comment - it is a good habit to document .mli files using the format supported by ocamldoc)
Such .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 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 { ... }
Case 3 would look like this:
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.
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 :
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 equal : ('a -> 'a -> bool) -> 'a list -> 'a list -> bool
val compare : ('a -> 'a -> int) -> 'a list -> 'a list -> int
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 concat_map : ('a -> 'b list) -> 'a list -> 'b list
val fold_left_map :
('a -> 'b -> 'a * 'c) -> 'a -> 'b list -> 'a * 'c 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 find_map : ('a -> 'b option) -> 'a list -> 'b option
val filter : ('a -> bool) -> 'a list -> 'a list
val find_all : ('a -> bool) -> 'a list -> 'a list
val filteri : (int -> 'a -> bool) -> 'a list -> 'a list
val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
val partition_map :
('a -> ('b, 'c) Either.t) -> 'a list -> 'b list * 'c 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
There is online documentation for each library.
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 equal : ('a -> 'a -> bool) -> 'a list -> 'a list -> bool
val compare : ('a -> 'a -> int) -> 'a list -> 'a list -> int
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 concat_map : ('a -> 'b list) -> 'a list -> 'b list
val fold_left_map :
('a -> 'b -> 'a * 'c) -> 'a -> 'b list -> 'a * 'c 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 find_map : ('a -> 'b option) -> 'a list -> 'b option
val filter : ('a -> bool) -> 'a list -> 'a list
val find_all : ('a -> bool) -> 'a list -> 'a list
val filteri : (int -> 'a -> bool) -> 'a list -> 'a list
val partition : ('a -> bool) -> 'a list -> 'a list * 'a list
val partition_map :
('a -> ('b, 'c) Either.t) -> 'a list -> 'b list * 'c 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 ...