A structure is simply a collection of definitions with name.
1 | module MyModule = struct |
The output has form module MyModule: sig ... end
Formally, the syntax is
1 | module ModuleName = module_expression |
Scope and Open
Once we have defined a module, we can access the members with . notation.
1 | module M = struct |
To make the second valid, we may “open a module”, similar to using namespace xxx; in C++, to bring definitions to outer space.
We can also open a module in module expression.
1 | module M = struct |
Thus, we may want to limit the scope of opening, for example, we may use Module.(e) grammar, which means that, all names used in e is from Module. Or the let open Module in e expression.
1 | (* Long *) |
Module Type Definition
OCaml will infer a signature as the type of a module, but we can also hand-write. For example,
1 | module type LIST_STACK = sig |
We then define the module, and tell OCaml that ListStack have the module type specified by LIST_STACK.
1 | module ListStack : LIST_STACK = struct |
Note that, the type must exactly fit. Otherwise, OCaml will report an error.
Semantics of Module Types
What does it mean when we write the :T in module M : T = ...? These are 2 properties that the compiler guarantees:
- Signature matching.
- Encapsulation. Any name in
Mbut not inTis not visible to code outside ofM.
Another important factor involves subtyping. For example, in Java, we may code like
1 | class C {} |
Subtyping is what permits the assignment of c <- d.
I thinks it’s like OCaml’s way of inheritence (as in C++, Java) or composition (as in Rust).
Suppose module type X = sig val x : int end, module type Z = sig val z : int end and module M = struct let x = 1;; let z = 2 end. It might be surprising to see module MXZ = ((M : X) : Z) is valid. But indeed, this notation implies an inheritence between X, Z.
Loading Compiled Modules
Suppose a file called mods.ml, and we have some code in it.
1 | let b = "bigred" |
Compiling with ocamlc mods.ml will generate a mods.cmo, a compiled module object file, which we can import in REPL with #load "mods.cmo";;. The module compiled is Mods, which is
1 | module Mods = struct |
Initializing at Top Level
We can use open instead of load to directly import names from module into toplevel, so we don’t need to Mods. every time.
1 | open Mods;; |
Requiring Libraries
To open OUnit2, we have to first load it first with require.
1 | #require "ounit2";; |
Encapsulation, Visibility with Modules
Visibility
1 | module type C_PUBLIC = sig |
Thus, we expose only y and hides x in module C.