Previous Up Next
5.2 Combining modules and classes


To benefit from the advantages of objects and modules simultaneously, an application can easily combine both aspect. Typically, modules will be used at the outer level, to provide separate compilation, inner structures, and privacy outside of the module boundaries, while classes will be components of modules, and offer extendibility, open recursion and late binding mechanisms.

We first present typical examples of such patterns, with increasing complexity and expressiveness. We conclude with a more complex ---but real--- example combining many features in an unusual but interesting manner.

5.2.1 Classes as module components

The easiest example is probably the use of modules to simply group related classes together. For instance, two classes nil and cons that are related by their usage, can be paired together in a module.
      
module Cell = struct
  exception Nil
  class ['anil =
    object (self : 'alist)
      method hd : 'a = raise Nil
      method tl : 'alist = raise Nil
      method null = true
    end;;
      
  class ['acons h t =
    object (_ : 'alist)
      val hd = h val tl = t
      method hd : 'a = h
      method tl : 'alist = t
      method null = false
    end;;
  end;;
Besides clarity of code, one quickly take advantage of such grouping. For instance, the nil and cons classes can be extended simultaneously (but this is not mandatory) to form an implementation of lists:
      
module List = struct
  class ['anil =
    object
      inherit ['aCell.nil
      method length = 0
    end;;
      
  class ['acons h t =
    object
      inherit ['aCell.cons h t
      method length = 1+tl#length
    end;;
  end;;
In turn, the module List can also be extended ---but in another direction--- by adding a new ``constructor'' append. This amounts to adding a new class with the same interface as that of the first two.
Remark 8   In OCaml, lists are more naturally represented by a sum data type, which moreover allows for pattern matching. However, datatypes are not extensible.

In this example, grouping could be seen as a structuring convenience, because a flattened implementation of all classes would have worked as well. However, grouping becomes mandatory for friend classes.

Friend classes
State encapsulation in objects allows to abstract their representation by hiding all instance variables. Thus, reading and writing capabilities can be controlled by providing only the necessary methods. However, whether to expose some given part of the state is an all-or-nothing choice: either it is confined to the object or revealed to the whole world.

It is often the case that some, but not all, objects can access each other's state. A typical example (but not the only one) are objects with binary methods. A binary method of an object is called with another object of the same class as argument, so as to interact with it. In most cases, this interaction should be intimate, eg. depend on the details of their representations and not only on their external interfaces. For instance, only objects having the same implementation could be allowed to interact. With objects and classes, the only way to share the representation between two different objects is to expose it to the whole world.

Modules, which provide a finer-grain abstraction mechanism, can help secure this situation, making the type of the representation abstract. Then, all friends (classes or functions) defined within the same module and sharing the same abstract view know the concrete representation.

This can be illustrated on the bank example, by turning currency into a class:
      
module type CURRENCY = sig
  type t
  class c : float ->
    object ('a)
      method v : t
      method plus : 'a -> 'a
      method prod : float -> 'a
    end
end;;
module Currency =  struct
  type t = float
  class c x =
    object (_ : 'a)
      val v = x method v = v
      method plus(z:'a) = {< v = v +. z#v >}
      method prod x = {< v = x *. v >}
    end
end;;
module Euro = (Currency : CURRENCY);;
Then, all object of the class Euro.c can be combined, still hiding the currency representation.

A similar situation arises when implementing sets with a union operation, tables with a merge operation, etc.

5.2.2 Classes as pre-modules

We end this Chapter with an example that interestingly combines some features of classes objects and modules. This example is taken from the algebraic-structure library of the formal computation system FOC [7]. The organization of such a library raises important problems: on the one hand, algebraic structures are usually described by successive refinements (a group is a monoid equipped with an additional inverse operation). The code structure should reflect this hierarchy, so that at least the code of the operations common to a structure and its derived structures can be shared. On the other hand, type abstraction is crucial in order to hide the real representations of the structure elements (for instance, to prevent from mixing integers modulo p and integers modulo q when p is not equal to q). Furthermore, the library should remain extensible.

In fact, we should distinguish generic structures, which are abstract algebraic structures, from concrete structures, which are instances of algebraic structures. Generic structures can either be used to derive richer structures or be instantiated into concrete structures, but they themselves do not contain elements. On the contrary, concrete structures can be used for computing. Concrete structures can be obtained from generic ones by supplying an implementation for the basic operations. This schema is sketched in figure 5.2. The arrows represent the expected code sharing.

In general, as well as in this particular example, there are two kinds of expected clients of a library: experts and final users. Indeed, a good library should not only be usable, but also re-usable. Here for instance, final users of the library only need to instantiate some generic structures to concrete ones and use these to perform computation. In addition, a few experts should be able to extend the library, providing new generic structures by enriching existing ones, making them available to the final users and to other experts.
Figure 5.2: Algebraic structures

The first architecture considered in the FOC project relies on modules, exclusively; modules facilitates type abstraction, but fails to provide code sharing between derived structures. On the contrary, the second architecture represents algebraic structures by classes and its elements by objects; inheritance facilitates code sharing, but this solution fails to provide type abstraction because object representation must be exposed, mainly to binary operations.

The final architecture considered for the project mixes classes and modules to combine inheritance mechanisms of the former with type abstraction of the latter. Each algebraic structure is represented by a module with an abstract type t that is the representation type of algebraic structure elements (ie. its ``carrier''). The object meth, which collects all the operations, is obtained by inheriting from the virtual class that is parameterized by the carrier type and that defines the derived operations. For instance, for groups, the virtual class ['a]group declares the basic group operations (equal, zero, plus, opposite) and defines the derived operations (not_equal, minus) once and for all:
      
class virtual ['agroup =
  object(self)
    method virtual equal: 'a -> 'a -> bool
    method not_equal x y = not (self#equal x y)
    method virtual zero: 'a
    method virtual plus: 'a -> 'a -> 'a
    method virtual opposite: 'a -> 'a
    method minus x y = self#plus x (self#opposite y)
  end;;
A class can be reused either to build richer generic structures by adding other operations or to build specialized versions of the same structure by overriding some operations with more efficient implementations. The late binding mechanism is then used in an essential way.

(In a more modular version of the group structure, all methods would be private, so that they can be later ignored if necessary. For instance, a group should be used as the implementation of a monoid. All private methods are made public, and as such become definitely visible, right before a concrete instance is taken.)

A group is a module with the following signature:
      
module type GROUP =
  sig
    type t
    val metht group
  end;;
To obtain a concrete structure for the group of integers modulo p, for example, we supply an implementation of the basic methods (and possibly some specialized versions of derived operations) in a class z_pz_impl. The class z_pz inherits from the class [int]group that defines the derived operations and from the class z_pz_impl that defines the basic operations. Last, we include an instance of this subclass in a structure so as to hide the representation of integers modulo p as OCaml integers.
      
class z_pz_impl p =
  object
    method equal (x : inty = (x = y)
    method zero = 0
    method plus x y = (x + ymod p
    method opposite x = p - 1 - x
  end;;
class z_pz p =
  object
    inherit [intgroup
    inherit z_pz_impl p
  end;;
module Z_pZ =
  functor (Xsig val p : int end) ->
    (struct
      type t = int
      let meth = new z_pz X.p
      let inj x =
        if x >= 0 && x < X.p then x else failwith "Z_pZ.inj"
      let proj x = x
    end : sig
      type t
      val metht group
      val injint -> t
      val projt -> int
    end);;
This representation elegantly combines the strengths of modules (type abstraction) and classes (inheritance and late binding).
Exercise 32  [Project ---A small subset of the FOC library]   As an exercise, we propose the implementation of a small prototype of the FOC library. This exercise is two-fold.

On the one hand, it should include more generic structures, starting with sets, and up to at least rings and polynomials.

On the other hand, it should improve on the model given above, by inventing a more sophisticated design pattern that is closer to the model sketched in figure 
5.2 and that can be used in a systematic way.

For instance, the library could provide both an open view and the abstraction functor for each generic structure. The open view is useful for writing extensions of the library. Then, the functor can be used to produce an abstract concrete structure directly from an implementation.

The pattern could also be improved to allow a richer structure (
eg. a ring) to be acceptable in place only a substructure is required (eg. an additive group).

The polynomials with coefficients in Z /2Z offers a simple yet interesting source of examples.


Previous Up Next