Previous Contents Next
2 Comparison with other proposals


The affirmed strength of virtual types is that they allow one to define families of classes (such as the subject/observer pair in the example above) whose instances are to interact with one another, in such a way that these classes can be extended by inheritance without limitation. Primitive solutions to virtual types are based on the mutually recursive definitions of classes of the same family.

2.1 The subject/observer pattern in [BOW98]


The general subject/observer pattern is implemented as follows in [BOW98] (we filled in some of the holes):
public interface SubjectObserverIfc {
  public interface ObserverIfc (O) { void at_notification (S s, E e); }
  public interface SubjectIfc (S) { void add_observer (@O o); void notify (E e); }
  public use Object (E);
}
public class SubjectObserver implements SubjectObserverIfc {
  public static class Subject (S) implements @SubjectIfc {
    protected @O observers []; public void add_observer (@O obs) { ... }
    public void notify (E e) {
      for (int i = 0; i < observers.length; i++)
        observers[i].at_notification(this, e);
    }
  }
  public static class Observer (O) implements @ObserverIfc  
    { public void at_notification (S s, E e) { } }
}
Classes Subject and Observer are defined as inner classes of the class SubjectObserver. The only way to inherit from the class SubjectObserver is to inherit and refine all elements together. Following our example from the previous section, we define a class of events to be used by WindowManager:
public class Event { 
    protected int a; public Event (int x) { this.a = x; }
    public int action () { return a; } 
}
public interface WindowManagerIfc extends SubjectObserverIfc {
  public interface WindowIfc (S) extends SubjectIfc {
    public void move (int d);
    public void draw;
  }
  public interface ManagerIfc (O) extends ObserverIfc { ... }
  public use Event (E);
}
public class WindowManager extends SubjectObserver implements WindowManagerIfc {
  public Window extends Subject {
    extends Subject implements SubjectIfc
    protected int position;
    public Window () { position = 0; }
    public void move (int d) { position = d;  this.notify (new Event(0)); }
    public void draw () { ... }
  }
  public Manager extends Observer {
    extends Observer implements WindowIfc
    public void at_notification (S s, E e) { if (e.action == 0) s.draw(); }
  }
}
The construction ``use'', introduced in the appendix of [BOW98], allows one to parameterize classes of the group by another class defined outside of the group (here the class Event). However, this construction, which is only sketched in [BOW98], remains unclear. In particular, the code above is certainly not correct. Indeed, the expression new event(0) has type Event and not E (since E is any type that extends Event). Hence, we should either definitely set the type E to be Event, or parameterize the class Subject over the creation of events as we did in section 1. The former will not allow further extension of the type of events. The later would more roughly follow our solution in Ocaml. However, further complications are induced. First, one need to provide an object instead of a function to the constructor Window (but this is only resulting from the absence of first class functions), using an auxiliary class MakeEvent. Second, and more cumbersome, the type of the class MakeEvent need also to be introduced with a use clause, since its type depend on the type of events. In other words, the construction use permits to import class types from the outside, but does not allow a contravariant use of the corresponding class.

2.2 The subject/observer pattern in [Tor98]


The subject/observer pattern cannot apparently be implemented with virtual types as in Torgersen's proposal [Tor98] because of lack of block structure. However, one may assume an extension of [Tor98] with block structure and recursively defined classes (as in the language Beta [KMMPN87]); then, one should be able to solve the subject/observer pattern more or less as follows.
ObserverArray = Array { ELEMENT <= Observer; }
Subject = {
  A <= ObserverArray;  E <= Object; O <= Observer;
  observers : A;
  add_observer (obs : O) { ... }
  notify (E e) { 
    for (int i = 0; i < observers.length; i++) observers[i].at_notification(this, e);
    }
  }
Observer = {
  S <= Subject; E <= Object;
  at_notification (S s, E e) { }
}
These classes are all recursively defined, even though this is not syntactically apparent. However, only class types are recursive. Moreover, classes do not need to be grouped with a rigid structure. In particular, it is possible to inherit separately from any class.
Event = { a : Int; action () : Int { return a; } }
ManagerArray = ObserverArray { ELEMENT <= Manager; }
Window = Subject {
  A <= ManagerArray; E = Event; O <= Manager;
  position : Int;
  move (d : Int) { position = d; this.notify (new Event); }
  draw () { ... }
}
Manager = Observer {
  S <= Window;  E <= Event;
  at_notification (S s, E e) { if (e.action == 0) s.draw(); }
}
In class Window, the type E must be fixed to be exactly the type Event, as the method notify expects an object of type E and receives an object of type Event. This will prevent possible refinement of events in a subclass. While in section 2.1, this could be fixed by type abstraction, there does not seem to be a similar solution here.

It is not possible to directly create objects from classes Window or Manager, etc. since they contain open types (types that are not finally bound to a fixed type). Therefore, one has to subclass all of them:
RealManagerArray = ManagerArray { ELEMENT = RealManager; }
RealWindow = Window { A = RealManagerArray; O = RealManager }
RealManager = Manager { S = RealWindow; E = Event; }
Classes are more loosely coupled here than in section 2.1, since the scope of the recursive definition is less rigid than in the previous solution. However, one is forced to define a hierarchy of class generators that are used only for inheritance and to take final instances of the generators that are used only to build objects. In this sense, the solution resembles the encoding of virtual types in Pizza that is discussed in the next section and presents at least similar drawbacks.

2.3 De-coupling classes of a group

As was demonstrated by our example in section 1, classes observer and subject are independent, and do not need to be recursively defined. One advantage of loosely coupled classes is to be able to inherit separately from two distinct classes or define an independent class that will interact seamlessly with a previously defined class. On the contrary, with virtual types, classes have to be defined and inherited simultaneously in a recursive pattern that has been fixed once and for all (often called a group).

In section 1, we have shown two distinct situations where such a flexibility is useful. Indeed, one may refine the observer in several interesting ways O1, O2, O3 and, independently, implement several versions of the subject S1 and S2. In section 5, we show another argument against virtual types. Those examples demonstrate that there is a continuous path from conses to lists, via heterogenous lists. Alternating lists are then just a particular point along this path that appears simply as a type specialization of heterogeneous lists. Why should virtual types abruptly show up at this point? Worse, following the proposal [BOW98] it would not even be possible to specialize or inherit from the previous definitions at this particular point; instead, all classes would have to be redefined from scratch.

The more recent proposal [BV99] relaxes the coupling of classes in a group. They allow to inherit from a group and add new classes in the inherited group. However, it remains more rigid than our solution. For instance, by lack of multiple inheritance, they cannot apparently define a subject/observer group G and refine the observer class O of G in two independent observer classes O1 and O2 whose objects could all be connected to a same instance of the subject class S.

2.4 A pathological benefit of coupling

On the other hand, it has been argued that when the grouping is one of the goals aimed at, then a specific construct is more concise [Wad98, BV99]. Consider n mutually recursive classes, so that each class is parameterized over the types of the objects of the n-1 other classes. In short, the argument is that a group construct will allow the source code to be in O(n) while parametric classes take at least O(n2) space of source code. However, the reality is more complex. With parametric classes, a class need to be abstracted over the n-1 other classes only if there are constraints in the source code that force to do so. That is, the source code of each class should at least be in O(n) itself. Hence, both virtual types and parametric polymorphism would have source code in size O(n) . However, this is only true for the initial declaration of the group. When inheriting, the typing constraints will be duplicated with a simple inherit clause. Thus, our solution to virtual types might sometimes require larger code.

We think that such examples are pathological and that, in most cases, the number of type parameters will remain relatively small. In any case, a conclusion could only be drawn after a serious analysis of real-size examples written using both styles and not on a few sketches of programs. We should also emphasize that this is also partly a matter of syntactic sugar: recursive class definitions could be allowed to induce type abbreviations that could be used to shorten type expressions. The inconvenience of an occasional increase in the size of type annotations is also of lower importance than the serious limitation of expressiveness implied by the use of rigid grouping.


Previous Contents Next