The Module Language
The Objective CAML language features a sub-language for modules, which comes
in addition to the core language that we have seen so far. In this
module language, the interface of a module is called a
signature and its implementation is called a
structure. When there is no ambiguity, we will often use the
word ``module'' to refer to a structure.
The syntax for declaring signatures and structures is as follows:
Syntax
module type NAME = |
sig |
interface declarations |
end |
Syntax
module Name = |
struct |
implementation definitions |
end |
Warning
The name of a module must start with an uppercase letter. There
are no such case restrictions on names of signatures, but by
convention we will use names in uppercase for signatures.
Signatures and structures do not need to be bound to names: we can
also use anonymous signature and structure expressions, writing simply
Syntax
sig declarations end
Syntax
struct definitions end
We write signature and structure to refer to either
names of signatures and structures, or anonymous signature and
structure expressions.
Every structure possesses a default signature, computed by the type
inference system, which reveals all the definitions contained in the
structure, with their most general types. When defining a structure,
we can also indicate the desired signature by adding a signature constraint
(similar to the type constraints from the core language), using one of
the following two syntactic forms:
Syntax
module Name : signature =
structure
Syntax
module Name =
(structure : signature)
When an explicit signature is provided, the system checks that all the
components declared in the signature are defined in the structure
structure, and that the types are consistent. In other terms,
the system checks that the explicit signature provided is ``included
in'', or implied by, the default signature. If so, Name is
viewed in the remainder of the code with the signature
``signature'', and only the components declared in the signature
are accessible to the clients of the module. (This is the same behavior
we saw previously with interface files.)
Access to the components of a module is via the dot notation:
Syntax
Name1.name2
We say that the name name2 is qualified by the name
Name1 of its defining module.
The module name and the dot can be omitted using a directive to
open the module:
Syntax
open Name
In the scope of this directive, we can use short names name2
to refer to the components of the module Name. In case of name
conflicts, opening a module hides previously defined entities with the
same names, as in the case of identifier redefinitions.
Two Stack Modules
We continue the example of stacks by recasting it in the module
language. The signature for a stack module is obtained by wrapping
the declarations from the stack.mli
file in a signature
declaration:
# module
type
STACK
=
sig
type
'a
t
exception
Empty
val
create:
unit
->
'a
t
val
push:
'a
->
'a
t
->
unit
val
pop:
'a
t
->
'a
val
clear
:
'a
t
->
unit
val
length:
'a
t
->
int
val
iter:
('a
->
unit)
->
'a
t
->
unit
end
;;
module type STACK =
sig
type 'a t
exception Empty
val create : unit -> 'a t
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val clear : 'a t -> unit
val length : 'a t -> int
val iter : ('a -> unit) -> 'a t -> unit
end
A first implementation of stacks is obtained by reusing the Stack module from the standard library:
# module
StandardStack
=
Stack
;;
module StandardStack :
sig
type 'a t = 'a Stack.t
exception Empty
val create : unit -> 'a t
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val clear : 'a t -> unit
val length : 'a t -> int
val iter : ('a -> unit) -> 'a t -> unit
end
We then define an alternate implementation based on arrays:
# module
MyStack
=
struct
type
'a
t
=
{
mutable
sp
:
int;
mutable
c
:
'a
array
}
exception
Empty
let
create
()
=
{
sp=
0
;
c
=
[||]
}
let
clear
s
=
s.
sp
<-
0
;
s.
c
<-
[||]
let
increase
s
x
=
s.
c
<-
Array.append
s.
c
(Array.create
5
x)
let
push
x
s
=
if
s.
sp
>=
Array.length
s.
c
then
increase
s
x;
s.
c.
(s.
sp)
<-
x;
s.
sp
<-
succ
s.
sp
let
pop
s
=
if
s.
sp
=
0
then
raise
Empty
else
(s.
sp
<-
pred
s.
sp
;
s.
c.
(s.
sp))
let
length
s
=
s.
sp
let
iter
f
s
=
for
i
=
pred
s.
sp
downto
0
do
f
s.
c.
(i)
done
end
;;
module MyStack :
sig
type 'a t = { mutable sp: int; mutable c: 'a array }
exception Empty
val create : unit -> 'a t
val clear : 'a t -> unit
val increase : 'a t -> 'a -> unit
val push : 'a -> 'a t -> unit
val pop : 'a t -> 'a
val length : 'a t -> int
val iter : ('a -> 'b) -> 'a t -> unit
end
These two modules implement the type t of stacks by different
data types.
# StandardStack.create
()
;;
- : '_a StandardStack.t = <abstr>
# MyStack.create
()
;;
- : '_a MyStack.t = {MyStack.sp=0; MyStack.c=[||]}
To abstract over the type representation in Mystack, we add a
signature constraint by the STACK signature.
# module
MyStack
=
(MyStack
:
STACK)
;;
module MyStack : STACK
# MyStack.create()
;;
- : '_a MyStack.t = <abstr>
The two modules StandardStack and MyStack
implement the same interface, that is, provide the same set of
operations over stacks, but their t types are different. It
is therefore impossible to apply operations from one module to values
from the other module:
# let
s
=
StandardStack.create()
;;
val s : '_a StandardStack.t = <abstr>
# MyStack.push
0
s
;;
Characters 15-16:
This expression has type 'a StandardStack.t = 'a Stack.t
but is here used with type int MyStack.t
Even if both modules implemented the t type by the same
concrete type, constraining MyStack by the signature
STACK suffices to abstract over the t type,
rendering it incompatible with any other type in the system and
preventing sharing of values and operations between the various stack
modules.
# module
S1
=
(
MyStack
:
STACK
)
;;
module S1 : STACK
# module
S2
=
(
MyStack
:
STACK
)
;;
module S2 : STACK
# let
s
=
S1.create
()
;;
val s : '_a S1.t = <abstr>
# S2.push
0
s
;;
Characters 10-11:
This expression has type 'a S1.t but is here used with type int S2.t
The Objective CAML system compares abstract types by names. Here, the two
types S1.t and S2.t are both abstract, and have
different names, hence they are considered as incompatible. It is
precisely this restriction that makes type abstraction effective, by
preventing any access to the definition of the type being abstracted.
Modules and Information Hiding
This section shows additional examples of signature constraints hiding
or abstracting definitions of structure components.
Hiding Type Implementations
Abstracting over a type ensures that the only way to construct values
of this type is via the functions exported from its definition
module. This can be used to restrict the values that can belong to
this type. In the following example, we implement an abstract type of
integers which, by construction, can never take the value 0.
# module
Int_Star
=
(
struct
type
t
=
int
exception
Isnul
let
of_int
=
function
0
->
raise
Isnul
|
n
->
n
let
mult
=
(
*
)
end
:
sig
type
t
exception
Isnul
val
of_int
:
int
->
t
val
mult
:
t
->
t
->
t
end
)
;;
module Int_Star :
sig type t exception Isnul val of_int : int -> t val mult : t -> t -> t end
Hiding Values
We now define a symbol generator, similar to that of page
??, using a signature constraint to hide the
state of the generator.
We first define the signature GENSYM exporting only two
functions for generating symbols.
# module
type
GENSYM
=
sig
val
reset
:
unit
->
unit
val
next
:
string
->
string
end
;;
We then implement this signature as follows:
# module
Gensym
:
GENSYM
=
struct
let
c
=
ref
0
let
reset
()
=
c:=
0
let
next
s
=
incr
c
;
s
^
(string_of_int
!
c)
end;;
module Gensym : GENSYM
The reference c holding the state of the generator
Gensym is not accessible outside the two exported functions.
# Gensym.reset();;
- : unit = ()
# Gensym.next
"T"
;;
- : string = "T1"
# Gensym.next
"X"
;;
- : string = "X2"
# Gensym.reset();;
- : unit = ()
# Gensym.next
"U"
;;
- : string = "U1"
# Gensym.c;;
Characters 0-8:
Unbound value Gensym.c
The definition of c is essentially local to the structure
Gensym, since it is hidden by the associated signature.
The signature constraint achieves more simply the same goal as the
local definition of a reference in the definition of the two functions
reset_s and new_s on page ??.
Multiple Views of a Module
The module language and its signature constraints support taking
several views of a given structure. For instance, we can have a
``super-user interface'' for the module Gensym, allowing the
symbol counter to be reset, and a ``normal user interface'' that
permits only the generation of new symbols, but no other intervention
on the counter. To implement the latter interface, it suffices to
declare the signature:
# module
type
USER_GENSYM
=
sig
val
next
:
string
->
string
end;;
module type USER_GENSYM = sig val next : string -> string end
We then implement it by a mere signature constraint.
# module
UserGensym
=
(Gensym
:
USER_GENSYM)
;;
module UserGensym : USER_GENSYM
# UserGensym.next
"U"
;;
- : string = "U2"
# UserGensym.reset()
;;
Characters 0-16:
Unbound value UserGensym.reset
The UserGensym module fully reuses the code of the
Gensym module. In addition, both modules share the same counter:
# Gensym.next
"U"
;;
- : string = "U3"
# Gensym.reset()
;;
- : unit = ()
# UserGensym.next
"V"
;;
- : string = "V1"
Type Sharing between Modules
As we saw on page ??, abstract types with different
names are incompatible. This can be problematic when we wish to share
an abstract type between several modules. There are two ways to
achieve this sharing: one is via a special sharing construct in the
module language; the other one uses the lexical scoping of modules.
Sharing via Constraints
The following example illustrates the sharing issue. We define a
module M providing an abstract type M.t. We then
restrict M on two different signatures exporting different
subsets of operations.
# module
M
=
(
struct
type
t
=
int
ref
let
create()
=
ref
0
let
add
x
=
incr
x
let
get
x
=
if
!
x>
0
then
(decr
x;
1
)
else
failwith
"Empty"
end
:
sig
type
t
val
create
:
unit
->
t
val
add
:
t
->
unit
val
get
:
t
->
int
end
)
;;
# module
type
S1
=
sig
type
t
val
create
:
unit
->
t
val
add
:
t
->
unit
end
;;
# module
type
S2
=
sig
type
t
val
get
:
t
->
int
end
;;
# module
M1
=
(M:
S1)
;;
module M1 : S1
# module
M2
=
(M:
S2)
;;
module M2 : S2
As written above, the types M1.t and M2.t are
incompatible. However, we would like to say that both types are
abstract but identical. To do this, Objective CAML offers special syntax to
declare a type equality over an abstract type in a signature.
Syntax
NAME with
type t1 = t2
and ...
This type constraint forces the type t1 declared in the
signature NAME to be equal to the type t2.
Type constraints over all types exported by a sub-module can be declared
in one operation with the syntax
Syntax
NAME with module Name1 = Name2
Using these type sharing constraints, we can declare that the two
modules M1 and M2 define identical abstract types.
# module
M1
=
(M:
S1
with
type
t
=
M.t)
;;
module M1 : sig type t = M.t val create : unit -> t val add : t -> unit end
# module
M2
=
(M:
S2
with
type
t
=
M.t)
;;
module M2 : sig type t = M.t val get : t -> int end
# let
x
=
M1.create()
in
M1.add
x
;
M2.get
x
;;
- : int = 1
Sharing and Nested Modules
Another possibility for ensuring type sharing is to use nested
modules. We define two sub-modules (M1 et M2)
sharing an abstract type defined in the enclosing module M.
# module
M
=
(
struct
type
t
=
int
ref
module
M_hide
=
struct
let
create()
=
ref
0
let
add
x
=
incr
x
let
get
x
=
if
!
x>
0
then
(decr
x;
1
)
else
failwith
"Empty"
end
module
M1
=
M_hide
module
M2
=
M_hide
end
:
sig
type
t
module
M1
:
sig
val
create
:
unit
->
t
val
add
:
t
->
unit
end
module
M2
:
sig
val
get
:
t
->
int
end
end
)
;;
module M :
sig
type t
module M1 : sig val create : unit -> t val add : t -> unit end
module M2 : sig val get : t -> int end
end
As desired, values created by M1 can be operated upon by
M2, while hiding the representation of these values.
# let
x
=
M.
M1.create()
;;
val x : M.t = <abstr>
# M.
M1.add
x
;
M.
M2.get
x
;;
- : int = 1
This solution is heavier than the previous solution based on type
sharing constraints: the functions from M1 and M2
can only be accessed via the enclosing module M.
Extending Simple Modules
Modules are closed entities, defined once and for all. In particular,
once an abstract type is defined using the module language, it is
impossible to add further operations on the abstract type that depend
on the type representation without modifying the module definition itself.
(Operations derived from existing operations can of course be added
later, outside the module.) As an extreme example,
if the module exports no creation function, clients of the module
will never be able to create values of the abstract type!
Therefore, adding new operations that depend on the type
representation requires editing the sources of the module and adding
the desired operations in its signature and structure. Of course, we
then get a different module, and clients need to be recompiled.
However, if the modifications performed on the module signature did
not affect the components of the original signature, the remainder of
the program remains correct and does not need to be modified, just recompiled.