For the past few classes we have been considering abstraction and
modular design, primarily through the use of the module
mechanism in OCaml. We have seen that good design principles include
writing clear specifications of interfaces, independent of the actual
implementation. We have also seen that writing good documentation of
the implementation is important. Today we will consider another means
of abstraction called functors, that enable modules to be
combined together by parameterizing a module in terms of other
modules.
Consider the set data abstraction that we have looked at during the past few classes:
module type SETSIG = sig type 'a set val empty : 'a set val add : 'a -> 'a set -> 'a set val mem : 'a -> 'a set -> bool val rem : 'a -> 'a set -> 'a set val size: 'a set -> int val union: 'a set -> 'a set -> 'a set val inter: 'a set -> 'a set -> 'a set end
While this interface uses polymorphism to enable sets with
different types of elements to be created, any implementation of this
signature needs to use the built-in =
function in testing
whether an element is a member of such a set. Thus we cannot for
example have a set of strings where comparison of the elements is done
in a case-insensitive manner, or a set of integers where elements are
equal when their magnitudes are equal (i.e., their absolute values are
equal). We could write two separate signatures, one for sets with
string elements and one for sets with integer elements, and then in
the implementation of each signature use an appropriate comparison
function. However this would yield a lot of nearly duplicated code,
both in the signatures and in the implementation. Such nearly
duplicated code is more work to write and maintain and more
importantly is often a source of bugs when things are changed in one
place and not another.
A functor is a module that is parameterized by other modules. Functors will allow us to create a set module that is parameterized by another module that does equality testing, thereby allowing the same code to be used for different equality tests. To make this concrete we will consider an example with the following simple interface for sets:
module type SETSIG = sig type set type elt val empty : set val mem : elt -> set -> bool val add: elt -> set -> set val find: elt -> set -> elt end
Note that this interface differs from the one above, in that it
defines a set of a given fixed element type, rather than an 'a
set
. It also defines just three operations, although others
could easily be added.
In addition to a set abstraction, we also need another module that abstracts the comparison operation on the elements of the set, which will be used to parameterize the set module. The signature for this type comparator is simply a type and a comparison function:
module type EQUALSIG = sig type t val equal : t -> t -> bool end
Now we are ready to define a functor implementing
the SETSIG
signature. Unlike other module
implementations we have seen, this module will not be instantiated
directly, but rather will be used to define modules that are
instantiated. Thus it is an abstract "template" that defines set
modules in terms of equality testing modules with
the EQUALSIG
signature. Here is the definition,
understanding it will take a bit of discussion:
module MakeSet (Equal : EQUALSIG) : SETSIG with type elt = Equal.t = struct open Equal type elt = t type set = elt list let empty = [] let mem x s = List.exists (equal x) s let add x s = if mem x s then s else x :: s let find x s = List.find (equal x) s end
First note that before the specification of the
signature SETSIG
being implemented here, there is the
expression (Equal : EQUALSIG)
. This means that the
module MakeSet
is parameterized by a module with
signature EQUALSIG
and this module will be referred to
using the name Equal
in the body of the module
definition. In general there can be any number of modules
parameterizing a module, each of which must be specified in
parentheses with a name that will be used in the body together with
the type signature of the module. Note that these parameters to a
module can only be modules (including functors), they cannot be
first-class objects of the language such as functions or other types.
Further note that after the specification of
the SETSIG
signature is the expression
with type elt = Equal.t =
This expression specifies that these two types are shared. When
combining different modules together using functors, often types in
one module must be the same as types in the other module. In the
current example the elt
type of the SETSIG
signature must be the same as the t
type of
the EQUALSIG
signature. In general there can be any
number of such sharing constraints between types.
The body of the MakeSet
module is like the body of any
other module. In this example the open
directive is used
so that the names t
and equal
can be
referred to without qualifying them as Equal.t
and Equal.equal
.
It is also worth noting the partial uncurrying
of equal
for instance in:
let mem x s = List.exists (equal x) s
which returns a function that tests whether a given element is
equal to the value of x
, and that function is used
by List.exists
.
Note that the MakeSet
module (and any functor) is
abstract. We use the functor to create a module, and then use that
resulting module. The MakeSet
module itself does not
define operations like a standard module. For instance there is
no MakeSet.add
, but there will be an add
operation in whatever modules are created using MakeSet
.
In order to use MakeSet
we need an implementation of a
module with the EQUALSIG
signature. Here is such an
implementation for testing equality of strings in a case independent
fashion:
module StringNoCase = struct type t = string let equal s1 s2 = String.lowercase s1 = String.lowercase s2 end
Now we can use MakeSet
to create a string set module
with case insenstitive equality:
module SSet = MakeSet (StringNoCase)
Evaluating this expression the interpreter prints out:
module SSet : sig type set = MakeSet(StringNoCase).set type elt = StringNoCase.t val empty : set val mem : elt -> set -> bool val add : elt -> set -> set val find : elt -> set -> elt end
That is, the SSet
module defines the
types set
and elt and the
function
mem
, add
, and find
.
Now we can use this set abstraction to create and manipulate sets of strings, with case insensitive comparison of elements in a set.
# let s = SSet.add "I like CS 3110" SSet.empty;; val s : SSet.set =# SSet.mem "i LiKe cs 3110" s;; - : bool = true # SSet.find "i LiKe cs 3110" s;; - : SSet.elt = "I like CS 3110"
Now creating a module for sets of integers using absolute value
comparison involves almost no additional code. All that is necessary
is to create a module with the EQUALSIG
signature and
then use that as the parameter to MakeSet
:
module IntAbs = struct type t = int let equal i1 i2 = (abs i1) = (abs i2) end module ISet = MakeSet (IntAbs)
Now we can use this set abstraction to create and manipulate sets of integers, with absolute value comparison of elements in a set:
# let i = ISet.add 1 ISet.empty;; val i : ISet.set =# ISet.mem (-1) i;; - : bool = true # ISet.find (-1) i;; - : ISet.elt = 1