Previous Contents Next

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.


Previous Contents Next