Lecture 9: Functors — Parameterized Modules

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

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

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

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 =
  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

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

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 :
    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

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)

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