Previous Up Next

Chapter 4  The module language

Independently of classes, Objective Caml features a powerful module system, inspired from the one of Standard ML.

The benefits of modules are numerous. They make large programs compilable by allowing to split them into pieces that can be separately compiled. They make large programs understandable by adding structure to them. More precisely, modules encourage, and sometimes force, the specification of the links (interfaces) between program components, hence they also make large programs maintainable and reusable. Additionally, by enforcing abstraction, modules usually make programs safer.

4.1  Using modules

Compared with other languages already equipped with modules such as Modular-2, Modula-3, or Ada, the originality of the ML module system is to be a small typed functional language “on top” of the base language. The ML module system can actually be parameterized by the base language, which need not necessarily be ML. Thus, it could provide a language for modules other base languages.

4.1.1  Basic modules

Basic modules are structures, ie. collections of phrases, written struct p1pn end. Phrases are those of the core language, plus definitions of sub modules module X = M and of module types module type T = S. Our first example is an implementation of stacks.

module Stack = struct type 'a t = {mutable elements : 'a list } let create () = { elements = [] } let push x s = s.elements <- x :: s.elements let pop s = match s.elements with h::t -> s.elements <- t; h | [] -> failwith "Empty stack" end;;

Components of a module are referred to using the “dot notation”:

let s = Stack.create () in Stack.push 1 s; Stack.push 2 s; Stack.pop s;;
- : int = 2

Alternatively, the directive open S allows to further skip the prefix and the dot, simultaneously: struct open S … (f x : t) … end. A module may also be a subcomponent of another module:

module T = struct module R = struct let x = 0 end let y = R.x + 1 end

The “dot notation” and open extends to and can be used in sub-modules. Note that the directive open T.R in a module Q makes all components of T.R visible to the rest of the module Q but it does not add these components to the module Q.

The system infers signatures of modules, as it infers types of values. Types of basic modules, called signatures, are sequences of (type) specifications, written sig s1sn end. The different forms of specifications are described in figure 4.1.

Figure 4.1:
Specification ofform
valuesval x : σ
abstract typestype t
manifest typestype t = τ
exceptionsexception E
classesclass z : objectend
sub-modulesmodule X : S
module typesmodule type T  [ = M]

For instance, the system's answer to the Stack example was:

module Stack : sig type 'a t = { mutable elements : 'a list; } val create : unit -> 'a t val push : 'a -> 'a t -> unit val pop : 'a t -> 'a end

An explicit signature constraint can be used to restrict the signature inferred by the system, much as type constraints restrict the types inferred for expressions. Signature constraints are written (M : S) where M is a module and S is a signature. There is also the syntactic sugar module X : S = M standing for module X = (M : S).

Precisely, a signature constraint is two-fold: first, it checks that the structure complies with the signature; that is, all components specified in S must be defined in M, with types that are at least as general; second, it makes components of M that are not components of S inaccessible. For instance, consider the following declaration:

module S : sig type t val y : t end = struct type t = int let x = 1 let y = x + 1 end

Then, both expressions S.x and S.y + 1 would produce errors. The former, because x is not externally visible in S. The latter because the component S.y has the abstract type S.t which is not compatible with type int.

Signature constraints are often used to enforce type abstraction. For instance, the module Stack defined above exposes its representation. This allows stacks to be created directly without calling Stack.create.

Stack.pop { Stack.elements = [2; 3]};;

However, in another situation, the implementation of stacks might have assumed invariants that would not be verified for arbitrary elements of the representation type. To prevent such confusion, the implementation of stacks can be made abstract, forcing the creation of stacks to use the function Stack.create supplied especially for that purpose.

module Astack : sig type 'a t val create : unit -> 'a t val push : 'a -> 'a t -> unit val pop : 'a t -> 'a end = Stack;;

Abstraction may also be used to produce two isomorphic but incompatible views of a same structure. For instance, all currencies are represented by floats; however, all currencies are certainly not equivalent and should not be mixed. Currencies are isomorphic but disjoint structures, with respective incompatible units Euro and Dollar. This is modeled in OCaml by a signature constraint.

module Float = struct type t = float let unit = 1.0 let plus = (+.) let prod = ( *. ) end;;
module type CURRENCY = sig type t val unit : t val plus : t -> t -> t val prod : float -> t -> t end;;

Remark that multiplication became an external operation on floats in the signature CURRENCY. Constraining the signature of Float to be CURRENCY returns another, incompatible view of Float. Moreover, repeating this operation returns two isomorphic structures but with incompatible types t.

module Euro = (Float : CURRENCY);; module Dollar = (Float : CURRENCY);;

In Float the type t is concrete, so it can be used for "float". Conversely, it is abstract in modules Euro and Dollar. Thus, Euro.t and Dollar.t are incompatible.

let euro x = x Euro.unit;; (euro 10.0) (euro 20.0);; (euro 50.0) Dollar.unit;;

Remark that there is no code duplication between Euro and Dollar.

A slight variation on this pattern can be used to provide multiple views of the same module. For instance, a module may be given a restricted interface in a given context so that certain operations (typically, the creation of values) would not be permitted.

module type PLUS = sig type t val plus : t -> t -> t end;; module Plus = (Euro : PLUS)
module type PLUS_Euro = sig type t = Euro.t val plus : t -> t -> t end;; module Plus = (Euro : PLUS_Euro)

On the left hand side, the type Plus.t is incompatible with Euro.t. On the right, the type t is partially abstract and compatible with Euro.t; the view Plus allows the manipulation of values that are built with the view Euro. The with notation allows the addition of type equalities in a (previously defined) signature. The expression PLUS with type t = Euro.t is an abbreviation for the signature

sig type t = Euro.t val plus: t -> t -> t end

The with notation is a convenience to create partially abstract signatures and is often inlined:

module Plus = (Euro : PLUS with type t = Euro.t);; Euro.unit Euro.unit;;
Separate compilation

Modules are also used to facilitate separate compilation. This is obtained by matching toplevel modules and their signatures to files as follows. A compilation unit A is composed of two files:

Another compilation unit B may access A as if it were a structure, using either the dot notation A.x or the directive open A. Let us assume that the source files are:, a.mli, That is, the interface of a B is left unconstrained. The compilations steps are summarized below:

ocamlc -c a.mliinterface of Aa.cmi
ocamlc -c a.mlimplementation of Aa.cmo
ocamlc -c b.mlimplementation of Bb.cmo
ocamlc -o myprog a.cmo b.cmolinking myprog

The program behaves as the following monolithic code:

module A : sig (* content of a.mli *) end = struct (* content of *) end module B = struct (* content of *) end

The order of module definitions correspond to the order of .cmo object files on the linking command line.

4.1.2  Parameterized modules

A functor, written functor (S : T) → M, is a function from modules to modules. The body of the functor M is explicitly parameterized by the module parameter S of signature T. The body may access the components of S by using the dot notation.

module M = functor(X : T) -> struct type u = X.t * X.t let y = X.g(X.x) end

As for functions, it is not possible to access directly the body of M. The module M must first be explicitly applied to an implementation of signature T.

module T1 = T(S1) module T2 = T(S2)

The modules T1, T2 can then be used as regular structures. Note that T1 et T2 share their code, entirely.

4.2  Understanding modules

We refer here to the literature. See the bibliography notes below for more information of the formalization of modules [27, 44, 45, 69].

For more information on the implementation, see [46].

4.3  Advanced uses of modules

In this section, we use the running example of a bank to illustrate most features of modules and combined them together.

Let us focus on bank accounts and, in particular, the way the bank and the client may or may not create and use accounts. For security purposes, the client and the bank should obviously have different access privileges to accounts. This can be modeled by providing different views of accounts to the client and to the bank:

module type CLIENT = (* client's view *) sig type t type currency val deposit : t -> currency -> currency val retrieve : t -> currency -> currency end;; module type BANK = (* banker's view *) sig include CLIENT val create : unit -> t end;;

We start with a rudimentary model of the bank: the account book is given to the client. Of course, only the bank can create the account, and to prevent the client from forging new accounts, it is given to the client, abstractly.

module Old_Bank (M : CURRENCY) : BANK with type currency = M.t = struct type currency = M.t type t = { mutable balance : currency } let zero = 0.0 M.unit and neg = (-1.0) let create() = { balance = zero } let deposit c x = if x > zero then c.balance <- c.balance x; c.balance let retrieve c x = if c.balance > x then deposit c (neg x) else c.balance end;; module Post = Old_Bank (Euro);; module Client : CLIENT with type currency = Post.currency and type t = Post.t = Post;;

This model is fragile because all information lies in the account itself. For instance, if the client loses his account, he loses his money as well, since the bank does not keep any record. Moreover, security relies on type abstraction to be unbreakable…

However, the example already illustrates some interesting benefits of modularity: the clients and the banker have different views of the bank account. As a result an account can be created by the bank and used for deposit by both the bank and the client, but the client cannot create new accounts.

let my_account = Post.create ();; Post.deposit my_account (euro 100.0); Client.deposit my_account (euro 100.0);;

Moreover, several accounts can be created in different currencies, with no possibility to mix one with another, such mistakes being detected by typechecking.

module Citybank = Old_Bank (Dollar);; let my_dollar_account = Citybank.create();;
Citybank.deposit my_account;; Citybank.deposit my_dollar_account (euro 100.0);;

Furthermore, the implementation of the bank can be changed while preserving its interface. We use this capability to build, a more robust —yet more realistic— implementation of the bank where the account book is maintained in the bank database while the client is only given an account number.

module Bank (M : CURRENCY) : BANK with type currency = M.t = struct let zero = 0.0 M.unit and neg = (-1.0) type t = int type currency = M.t type account = { number : int; mutable balance : currency } (* bank database *) let all_accounts = Hashtbl.create 10 and last = ref 0 let account n = Hashtbl.find all_accounts n let create() = let n = incr last; !last in Hashtbl.add all_accounts n {number = n; balance = zero}; n let deposit n x = let c = account n in if x > zero then c.balance <- c.balance x; c.balance let retrieve n x = let c = account n in if c.balance > x then (c.balance <- c.balance x; x) else zero end;;

Using functor application we can create several banks. As a result of generativity of function application, they will have independent and private databases, as desired.

module Central_Bank = Bank (Euro);; module Banque_de_France = Bank (Euro);;

Furthermore, since the two modules Old_bank and Bank have the same interface, one can be used instead of the other, so as to created banks running on different models.

module Old_post = Old_Bank(Euro) module Post = Bank(Euro) module Citybank = Bank(Dollar);;

All banks have the same interface, however they were built. In fact, it happens to be the case that the user cannot even observe the difference between either implementation; however, this would not be true in general. Indeed, such a property can not be enforced by the typechecker.

Exercise 31 (Polynomials with one variable)  
  1. Implement a library with operations on polynomials with one variable.

    The coefficients form a ring that is given as a parameter to the library.

  2. Use the library to check, for instance, the identity (1 + X) (1 − X) = 1 − X2.
  3. Check the equality (X + Y) (XY) = (X2Y2) by treating polynomials with two variables as polynomials with one variable X and where the coefficients are the ring of the polynomials with one variable Y.
  4. Write a program that reads a polynomial on the command line and evaluates it at each of the points given in stdin (one integer per line); the result should be printed in stdout.

Previous Up Next