5.3. Modules and the Toplevel#
Note
The video below uses the legacy build system, ocamlbuild, rather than the new build system, dune. Some of the details change with dune, as described in the text below.
There are several pragmatics involving modules and the toplevel that are important to master to use the two together effectively.
5.3.1. Loading Compiled Modules#
Compiling an OCaml file produces a module having the same name as the file, but
with the first letter capitalized. These compiled modules can be loaded into the
toplevel using #load
.
For example, suppose you create a file called mods.ml
, and put the following
code in it:
let b = "bigred"
let inc x = x + 1
module M = struct
let y = 42
end
Note that there is no module Mods = struct ... end
around that. The code is at
the topmost level of the file, as it were.
Then suppose you type ocamlc mods.ml
to compile it. One of the newly-created
files is mods.cmo
: this is a compiled module object file,
aka bytecode.
You can make this bytecode available for use in the toplevel with the following
directives. Recall that the #
character is required in front of a directive.
It is not part of the prompt.
# #load "mods.cmo";;
That directive loads the bytecode found in mods.cmo
, thus making a module
named Mods
available to be used. It is exactly as if you had entered this
code:
module Mods = struct
let b = "bigred"
let inc x = x + 1
module M = struct
let y = 42
end
end
module Mods :
sig val b : string val inc : int -> int module M : sig val y : int end end
Both of these expressions will therefore evaluate successfully:
Mods.b;;
Mods.M.y;;
- : string = "bigred"
- : int = 42
But this will fail:
inc
File "[3]", line 1, characters 0-3:
1 | inc
^^^
Error: Unbound value inc
Hint: Did you mean incr?
It fails because inc
is in the namespace of Mods
.
Mods.inc
- : int -> int = <fun>
Of course, if you open the module, you can directly name inc
:
open Mods;;
inc;;
- : int -> int = <fun>
5.3.2. Dune#
Dune provides a command to make it easier to start utop with libraries already
loaded. Suppose we add this dune file to the same directory as mods.ml
:
(library
(name mods))
That tells dune to build a library named Mods
out of mods.ml
(and any other
files in the same directory, if they existed). Then we can run this command
to launch utop with that library already loaded:
$ dune utop
Now right away we can access components of Mods
without having to issue
a #load
directive:
Mods.inc
- : int -> int = <fun>
The dune utop
command accepts a directory name as an argument if you want to
load libraries in a particular subdirectory of your source code.
5.3.3. Initializing the Toplevel#
If you are doing a lot of testing of a particular module, it can be annoying to have to type directives every time you start utop. You really want to initialize the toplevel with some code as it launches, so that you don’t have to keep typing that code.
The solution is to create a file in the working directory and call that file
.ocamlinit
. Note that the .
at the front of that filename is required and
makes it a hidden file that won’t appear in directory listings unless
explicitly requested (e.g., with ls -a
). Everything in .ocamlinit
will be
processed by utop when it loads.
For example, suppose you create a file named .ocamlinit
in the same directory
as mods.ml
, and in that file put the following code:
open Mods;;
Now restart utop with dune utop
. All the names defined in Mods
will already
be in scope. For example, these will both succeed:
inc;;
M.y;;
- : int -> int = <fun>
- : int = 42
5.3.4. Requiring Libraries#
Suppose you wanted to experiment with some OUnit code in utop. You can’t actually open it:
open OUnit2;;
File "[8]", line 1, characters 5-11:
1 | open OUnit2;;
^^^^^^
Error: Unbound module OUnit2
Hint: Did you mean Unit?
The problem is that the OUnit library hasn’t been loaded into utop yet. It can be with the following directive:
#require "ounit2";;
Now you can successfully load your own module without getting an error.
open OUnit2;;
5.3.5. Load vs Use#
There is a big difference between #load
-ing a compiled module file and
#use
-ing an uncompiled source file. The former loads bytecode and makes it
available for use. For example, loading mods.cmo
caused the Mod
module to be
available, and we could access its members with expressions like Mod.b
. The
latter (#use
) is textual inclusion: it’s like typing the contents of the
file directly into the toplevel. So using mods.ml
does not cause a Mod
module to be available, and the definitions in the file can be accessed
directly, e.g., b
.
For example, in the following interaction, we can directly refer to b
but
cannot use the qualified name Mods.b
:
# #use "mods.ml"
# b;;
val b : string = "bigred"
# Mods.b;;
Error: Unbound module Mods
Whereas in this interaction the situation is reversed:
# #directory "_build";;
# #load "mods.cmo";;
# Mods.b;;
- : string = "bigred"
# b;;
Error: Unbound value b
So when you’re using the toplevel to experiment with your code, it’s often
better to work with #load
rather than #use
. The #load
directive accurately
reflects how your modules interact with each other and with the outside world.