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.
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.
class ['a] cons 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 ['a] nil = object inherit ['a] Cell.nil method length = 0 end;;
class ['a] cons h t = object inherit ['a] Cell.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:
moduletype 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.
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 ['a] group = 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:
moduletype GROUP = sig type t val meth: t 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 : int) y = (x = y) method zero = 0 method plus x y = (x + y) mod p method opposite x = p - 1 - x end;; class z_pz p = object inherit [int] group inherit z_pz_impl p end;; module Z_pZ = functor (X: sig 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 meth: t group val inj: int -> t val proj: t -> 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.