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.
-
There is no reason for O and S to have the same number of refinements;
hence there is no reason for coupling the refinements Si's with
Oj's.
-
Once these classes are defined, one may choose any pairing, without having
to define new classes. Indeed, the type system will check that the pairing
makes sense, i.e. that each one of O and S implements the components
requested by the other.
-
Moreover, the different classes need not be in a subclassing hierarchy. For
instance, O3 could be derived directly from the root O, and still be
treated as any other observer.
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.