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, a construct that enables modules to be combined 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 SET = sig
  type 'a set
  val empty : 'a set
  val mem : 'a -> 'a set -> bool
  val add : 'a -> 'a set -> 'a set
  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 considered equal when their magnitudes (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 mapping from modules to modules. It allows the construction of a module parameterized by one or more other modules. Functors allow us to create a set module that is parameterized by another module that does the equality testing, thereby allowing the same code to be used for different equality tests. To make this concrete, we will consider an example using the following signatures:

module type EQUAL = sig
  type t
  val equal : t -> t -> bool
end
module type SETFUNCTOR =
  functor (Equal : EQUAL) ->
    sig
      type elt = Equal.t
      type set
      val empty : set
      val mem : elt -> set -> bool
      val add: elt -> set -> set
      val size: set -> int
    end

The signature EQUAL describes the input type for the functor. To implement EQUAL, a module need only specify a type t and a comparison function equal : t -> t -> bool, but these can be anything.

The signature SETFUNCTOR describes the type of the functor. This differs from the SET interface in several respects. First, the keyword functor indicates that it is a functor accepting a parameter, which in this case is any module of type EQUAL. Note how the syntax is reminiscent of the notation for functions. The parameter is referenced by the name Equal in the body of SETFUNCTOR, but that does not have to be its actual name.

The body of SETFUNCTOR describes the type of the module that will be produced. In the body, instead of the polymorphic 'a of SET, the type of the elements is named elt and is defined to be the same as the type t of the module Equal, whatever that is. There is also a fixed but unspecified type set, along with some set operations of the appropriate types, specified in terms of elt and set. (We have omitted a few of the operations for simplicity of the presentation, although they could easily be added back in.)

Now we are ready to define a functor implementing the SETFUNCTOR signature.

module MakeSet : SETFUNCTOR =
  functor (Equal : EQUAL) ->
    struct
      open Equal
      type elt = t
      type set = elt list
      let empty = []
      let mem x = List.exists (equal x)
      let add x s = if mem x s then s else x :: s
      let size = List.length
    end

First, the header

module MakeSet : SETFUNCTOR =

indicates that we are defining an implementation named MakeSet of the functor type SETFUNCTOR. The second line

  functor (Equal : EQUAL) ->

indicates that we are defining a functor with parameter Equal of type EQUAL. Again, the module implementing EQUAL is referenced by the name Equal in the body of MakeSet, but that does not have to be its actual name. In general there can be any number of parameter modules, each of which must be specified with a name and signature. Note that these parameters can only be modules, including other parameterized modules—they cannot be first-class objects of the language such as functions or other types.

Finally, the body of MakeSet between struct and end describes the implementation of the output module. This module must satisfy the signature described in the body of SETFUNCTOR.