On the (un)reality of virtual types
Didier Rémy and Jérôme Vouillon1
Mars, 2000, (in preparation) |
Résumé: Nous montrons, principalement sur des exemples détaillés, que des
schémas de programmation avec objets connus pour nécessiter
la notion de types virtuels
peuvent être écrits de façon directe et concise en utilisant le
polymorphisme paramétrique. Une amélioration significative que nous
apportons par rapport aux approches antérieures est de permettre à des
classes liées de pouvoir être définies indépendemment. Cette solution est
plus flexible, plus générale, et nous pensons, plus simple que les autres
solutions correctement typées qui ont été proposées.
Cette approche peut être utilisée dans plusieurs langages avec polymorphisme
paramétrique qui peuvent déja typer les méthodes binaires et possèsent des
types d'objets structurels.
Mots clés: Types Virtuels,
Types Structurels,
Types,
Objets,
Classes,
Modularité,
Ocaml
Abstract: We show, mostly through detailed examples, that object-oriented
programming patterns known
to involve the notion of virtual types can be implemented directly and
concisely using parametric polymorphism. A significant improvement we make
over previous approaches is to allow related classes to be defined
independently. This solution is more flexible, more general, and
we believe, simpler than other type-safe solutions previously proposed.
This approach can be applied to several languages with parametric
polymorphism that can already type binary methods and have structural
object types.
Keywords: Virtual Types,
Structural Types,
Types,
Objects,
Classes,
Modularity,
Ocaml
The programming patterns that lead to the introduction of virtual types are
not new in the object-oriented community. However, they recently became
famous for the typechecking challenges that they posed to some recent,
modern, and statically typed object-oriented languages, including several
extensions to Java [OW97a, Bru97]. The notion
of virtual types, first introduced in
Beta [KMMPN87], was not type-safe.
It is difficult to precisely define virtual types, since they have been
presented through programming patterns and (often incomplete) examples.
Some of them were written in informal extensions of existing languages.
Virtual types have never been given a semantic or syntactic
characterization.
Intuitively however, virtual types always involve the definition of several
related classes that interact with one another, such that at least the type
of objects of one of these classes depends on the type of objects of
another one;
moreover, the types of objects of these classes are often, but not
always, mutually recursive.
A few statically type-safe versions of virtual types have been
proposed [BOW98, Tor98].
In [BOW98] the authors show that
examples involving virtual types can be rewritten using only parametric
polymorphism. The language they used for this demonstration is Pizza ---an
extension of Java with parametric polymorphism [OW97a].
However, as the examples of virtual types written in Pizza are sometimes long
and tedious, the authors propose a new primitive construct to
allow a better treatment of virtual types. This includes a mechanism for
grouping together several class definitions. Classes of a group can be
inherited, but only simultaneously. The other work [Tor98] also introduces a primitive but different
construct.
In this paper, we argue as in [BOW98]
that virtual types can be encoded with parametric polymorphism. However, we
disagree with the introduction of a primitive notion of virtual types to
palliate the heaviness of their encoding in Pizza. Conversely, we claim
that no specific construct is necessary. We argue by giving a more direct
coding of examples involving virtual types. The approach is simpler, more
flexible, and more powerful than theirs.
In fact, using Ocaml [Ler96, RV98] as
the implementation language, all examples that we know as involving virtual
types can actually be typed in with no effort at all. Actually, the untyped
code that one naturally writes needs only to be documented with class type
parameters and, sometimes, with type constraints. No other change in the
code is necessary.
Had we not known about this challenge, we would not have
noticed any particular difficulty.
Through detailed examples and comparisons with other solutions, we are still
able to identify the potential difficulty of virtual types. An obvious
requirement is to have self-types in the language, i.e. the ability to type
self with an open object type (partially known object types). However,
mutual recursion between open object types is also needed in many examples
of virtual types. Then, the key is to be able to leave the recursion at the
level of types such that classes do not have to be defined recursively.
In Ocaml, this is permitted by the use of structural object types,
and of row variables [Rém94, Wan87] to keep
the monomorphic, recursive structure isolated from the unconstrained,
polymorphic row variables.
In fact, we are also able to transpose our solution to a few other languages
languages that posses structural types and that can already handle binary
methods.
In the language F<:w [Car90, SP94], we
only need to assume some standard class-based object-oriented extension, but
nothing else. Languages based on match-bounded quantification [BSvG95, BFP97] or F-bounded
quantification [CCH+89] need to be extended with
recursively defined bounds. For instance, this has been recently been done
for the language LOOM in [BV99]. On the
other hand, languages such as Pizza or GJ that already have recursive
F-bounded quantification need to be extended with structural object types,
only.
The rest of the paper is organized as follows. In section 1, we describe in detail the subject/observer
pattern [GHJV95] in Ocaml; we explain the typing constraints
and show the most general class types that are automatically inferred in
Ocaml. In section 2, we compare our solution to other
solutions in strongly-typed languages. We discuss the drawbacks of tightly
grouping definitions of related classes as do virtual types; we show that
enforcing a fixed recursive schema is too rigid (it imposes some
redundancies) and are too restrictive (some inheritance patterns cannot be
expressed). In section 4, we consider another example of
virtual types known as the ``cow/animal'', and discuss the issues of
expressiveness and abstraction. We also show that the lack of abstraction
does not clearly reduce expressiveness. In section 5, we use
the example of alternating lists to argue that while parametric types are
preferable to virtual types: parametric types allow a continuous refinement
of a hierarchy of classes while virtual types show up abruptly at a
particular point in the hierarchy.
1 |
The detailed example of subject/observers |
|
Virtual types are well illustrated by the example of the subject/observer
programming pattern. We simultaneously describe this example, its code, and
its typing in Ocaml. Since Ocaml has type inference, the user does not need
to write types except type parameters of classes and
appropriate type constraints to bind them in class bodies.
All the examples
of this section are written in the language Ocaml [Ler96]
and have been processed automatically by the toplevel interaction loop. We
only show the output in italic when needed.
The subject/observer example can be used when one or several observers need
to watch the state of one or several subjects or, more generally, when
objects of one class need to pass themselves to objects of another class
where both classes need to remain extensible.
We first define the skeleton of the pattern that implements the interaction
between the subject and the observer ignoring all other operations.
The subject reports actions to its
observers. Thus, it must remember the list of its observers in a field
variable. It should also provide some methods to add or remove observers.
More importantly, the subject has a distinguished method notify
used to
forward information to all of its observers (meanwhile adding itself as an
extra argument so that the observers can call the subject back).
'O'E
class ['O, 'E] subject =
object (self)
val mutable observers : 'O list = []
method add_observer obs = observers <- obs :: observers
method notify (e : 'E) =
List.iter (fun obs -> obs#at_notification self e) observers
end;;
class ['a, 'b] subject :
object ('c)
constraint 'a = < at_notification : 'c -> 'b -> unit; .. >
val mutable observers : 'a list
method add_observer : 'a -> unit
method notify : 'b -> unit
end
The inferred type of class subject
is parameterized over two variables
'a
and 'b
standing for the types of observers and of watched events,
and self type 'c
.
The type of observers 'a
is constrained to be an object type with
at least a method at_notification
of type 'c -> 'b -> unit
. The
dots ``..
'' in the constraint are a so-called row variable [Wan87, Rém94] that is kept anonymous in Ocaml for
simplicity of presentation. Thus, 'a
may be the type of an object with
more methods, for instance in a subclass. The class also has a field
observers
of type 'a list
and two
methods add_observer
and notify
of respective types
'a -> unit
and 'b -> unit
. It is also implicit from the class
type that the type of self bound to 'c
is the type of an object with
at least two methods add_observers
and notify
with their
respective types.
The class observer
is simpler. Mainly, it possesses a method
at_notification
to receive information from the subjects and
determine what to do next. The default behavior of the method
at_notification
is to do nothing, and the method will likely be
refined in subclasses.
class ['S, 'E] observer = object method at_notification (s : 'S) (e : 'E) = () end;;
The difficulty of this pattern usually lies in preserving the ability to
inherit from this general pattern to create new, real instances. This is
easy in Ocaml, as we illustrate on the example of a window manager. We
first define the type of events used to communicate between a window and a
manager. We make the event an object with an action
property, so that
the type of events can still be refined in subclasses2 (In Ocaml, it would be more
appropriate to use open variants to represent events, which we show in
appendix ; we keep the full
object-oriented solution here to ease comparison with other languages.)
type action = Move | Lower | Raise;;
class event x = object method action : action = x end;;
The window executes all
sorts of graphical operations, including its own display; whenever
necessary, the window will notify the manager that some particular task has
been completed. To allow further refinements of events, we abstract
the class window over a function that coerces actions to events.
class ['O, 'E] window (event : action -> 'E) =
object (self)
inherit ['O, 'E] subject
val mutable position = 0
method move d = position <- position + d; self#notify (event Move)
method draw = Printf.printf "[Position = %d]" position;
end;;
The manager watches events and defines the next task
to be accomplished depending on both the event and its sender. This may
include replying to the sender. For instance, when the observer is informed
that the window has moved, it may tell the window to redisplay itself.
class ['S, 'E] manager =
object
inherit ['S, 'E] observer
method at_notification s e = match e#action with Move -> s#draw | _ -> ()
end;;
Here is an example:
let window = new window (new event) in
window#add_observer (new manager); window#move 1;;
[Position = 1]- : unit = ()
Classes window
and manager
can be further extended by inheritance. We
first refine the type of events as suggested above. Then, we refine the
window class with a new behavior.
class big_event b x = object inherit event x method resized : bool = b end;;
class ['O] big_window() =
object (self)
inherit ['O, big_event] window (new big_event false) as super
val mutable size = 1
method resize x = size <- size + x; self#notify (new big_event true Raise)
method draw = super#draw; Printf.printf "[Size = %d]" size;
end;;
Here, we have definitely fixed the type of events for simplicity; of course,
we could have kept it parametric as we did for simple windows. The behavior
of the manager can also be refined, independently.
class ['S] big_manager =
object
inherit ['S, big_event] manager as super
method at_notification s e =
if e#resized then s#draw else super#at_notification s e
end;;
Note that the big windows are not bound to be managed by big managers.
In particular, one may freely choose any of the following combinations
(the missing one would fail to type, since a big manager requires its
window to have a method draw
):
(new window (new big_event false))#add_observer (new manager);;
(new big_window())#add_observer (new big_manager);;
(new big_window())#add_observer (new manager);;
Classes manager
and window
are defined independently. This is
important, since otherwise every combination would require a new definition.
More interestingly, we can also define another, entirely unrelated,
observer. This strengthens the idea that the class window
should not
be paired with the class manager
.
For instance, it is easy to implement an observer that simply traces all
events.
class ['S] trace_observer =
object
inherit ['S, big_event] observer
method at_notification s e =
Printf.printf "<%s>"
(match e#action with Lower -> "L" | Raise -> "R" | Move -> "M")
end;;
Here is an example combining all features:
let w = new big_window() in
w#add_observer (new big_manager); w#add_observer (new trace_observer);
w#resize 2; w#move 1;;
<R>[Position = 0][Size = 3]<M>[Position = 1][Size = 3]- : unit = ()
The example could be continued with many variations and refinements.
We also show another, safer implementation of the subject/observer in
appendix A.
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.
3 |
The necessary ingredients for decoupling classes |
|
As shown in section 1, classes do not need to be
recursive. Object types, however, often need to be mutually recursive. For
instance, the type of class subject
in section 1
has the following polymorphic class type, which uses recursive object types:
|
" b, ha, hc. |
let rec type a = á m : c ® b ® unit; hañ |
and c = á m1 : a ® unit; m2 : b ® unit; hcñ in |
class (c)[u : a; m1 : a ® unit; m2 : b ® unit] |
|
We used letrec type .. and .. in ..
rather than a standard recursive
µ-binder both to emphasize the mutual dependence and to reduce the
verbosity (expanding the former into the later would force some
duplication).
Using row-variables
The above type class can be written as such in Ocaml. This is made possible
by the use of row variables, such as hc and ha (in fact, in
Ocaml, ha would be left anonymous, and hc would be left
implicit.)
The key point here is that the quantified row
variables are independent and unconstrained. Row variables (and the
corresponding rich structure of record-types) [Rém94, Wan87] allow one to grab exactly the part of the
object that is polymorphic: the row variables ha and hc occur
at the leaves of the types that are bound to a and c; therefore they
are external to the recursion.
Indeed, the language Ocaml is not the only one to allow to write such a
type. However, expressing this type in F<: fails. This is not so
surprising since virtual types include binary methods and system F<: is
not powerful enough to type binary methods. The problem is that
standard record or object types used in F<: are less expressive than
extensible-record types used for Ocaml object types: there is no way
to grab record polymorphism directly as an unconstrained polymorphic type
variable. Instead, one is forced to quantify a with a bound of the form
á m : c ® b ® unit; hañ and similarly for c; that
is, one is lead to write:
|
"b.
" a <: á m : c ® b ® unitñ.
" c <: á m1 : a ® unit; m2 : b ® unitñ. |
class (c)[u : a; m1 : a ® unit; m2 : b ® unit] |
|
The bound of a depends on c and conversely. Thus, the above type is
ill-formed. There is no way to order type variables so as to obtain a
well-formed type expression.
Programming in F<:w
The system F<:w [SP94] is sufficiently powerful to
abstract over, and thus isolate, the interior of a record type. The
recursion can then be broken by parameterizing the bound of one of these
types over the other one. Assuming an Ocaml-like class-based object-oriented
extension of F<:w:
|
All (b <: T) |
All (Ra<: Fun (c:*) á m : c ® b ® unitñ) |
All (Rc<: Fun (a:*) á m1 : a ® unit; m2 :
b ® unitñ) |
let rec type a = Rac and c = Rca in |
class (c)[u : a; m1 : a ® unit; m2 : b ® unit] |
|
The code of class subject would be:
class subject =
fun (E <: $\Top$)
fun (RO <: Fun (S <: $\Top$) <at_notification : S -> E -> unit>)
fun (RS <: Fun (O <: $\Top$) <add_observer : O -> unit; notify : E -> unit>)
letrec type O = RO S and S = RS O in
object (self : S)
val observers : O = []
method add_observers (x : O) = observers <- x :: observers;
method notify (e : E) =
List.iter (fun (obs: O) obs.at_notification self e) observers
end
This solution is unsurprising, since a similar pattern has been used to type
binary methods in F<:w [BCC+96]3.
We assumed an extension of F<:w with a class-based language, which
includes recursive types. This is to keep closer to the Ocaml
object-oriented layer. We should be able to write the subject/observer
example in plain F<:w encoding objects into records and existential
types, even though the lack of primitive classes will likely make the example
more verbose.
F-bounded quantification with recursively defined bounds |
|
F-bounded quantification has been used to type binary methods involving
recursion between a quantified type variable and its bound [CCH+89]. However, F-bounded quantification as
presented in [CCH+89] only allows this particular
case and does not help when the recursion extends to several bounds.
Assuming a simple extension of F-bounded quantification with
recursively defined bounds would allow one to replace
" (a <: ta). " (c <: tc). T
by " (a <: taÙ c <: tc). T.
Variables a and c can now appear in both of their bounds.
Note that, as opposed to system F<: or even F-bounded
quantification, ML with subtyping constraints inherently comes with
recursive F-bounded quantification (with multiple bi-directional
bounds) [EST95a]. Thus, an object-oriented
language based on type constraints such as [EST95b] should also easily handle virtual types.
The extension of F-bounded quantification to recursively defined bounds
has actually been used in the language Pizza [OW97b]. Thus, in principle, Pizza could allow the same
style of programming as ours.
Indeed, it was already shown in [BOW98]
that examples involving virtual types can be implemented with and only with
parametric polymorphism. However, the solution proposed in [BOW98] still enforces grouping:
Two related classes A and B are replaced by two groups of recursive
definitions. The first group defines two generators A' and B'
abstracted over classes X and Y that should be instances of
the generators, respectively:
public class A'<X extends A'<X,Y>, Y extends B'<X,Y>> { ... }
public class B'<X extends A'<X,Y>, Y extends B'<X,Y>> { ... }
Classes A and B are then defined as fix points of the
generators:
public class A extends A'<A,B> { public A (args) { super (args); } }
public class B extends B'<A,B> { public B (args) { super (args); } }
Inheritance must be applied to generators and must preserve the group
structure. The authors find this solution unsatisfactory. Consequently,
they present an extension to Java/Pizza to cope with virtual types more
directly. Hence, the authors chose to augment the coupling of related
classes, while we prefer to loose such coupling as much as possible.
One may still wonder whether decoupling related classes is possible in
Pizza, since its type system seems to have sufficient expressiveness. Below
is an (unsuccessful) attempt to write the above class type in Pizza:
|
class (b £ á ñ,
a £ á m : c ® b ® unitñ,
c £ á m1 : a ® unit; m2 : b ® unitñ
) |
[ u : a; m1 : a ® unit; m2 : b ® unit] |
|
Alas, Pizza object types cannot be built from scratch, but only as the types
of elements of classes. Hence, the above expression is ill-formed. An
expression with equivalent meaning can only be built using recursively
defined classes.
Adding structural types to Pizza would allow one to define a class with the
above type. For convenience, one could also add an anonymous fix point
construct so as to avoid the systematic need for the second group
definition. (Once grouping has been eliminated, this second step becomes
easier and should usually be a simple type application.)
Match-bounded quantification with recursively
defined bounds |
|
Match-bounded quantification [Bru95] can
solve binary methods as easily as row variables do.
However, it suffers from the same
problem as bounded-quantification in its inability to grab the interior of a
record type. In the subject/observer example, bounds would also need to be
recursively defined. This difficulty has motivated an extension of LOOM [BFP97, Bru97] with recursively defined bounds, which
actually lead to the
proposal presented in [BOW98].
Again, we claim that match-bounded quantification with multiple recursive
bounds does not have to enforce grouping. More recently,
Bruce and Vanderwaart also proposed a notion of type-group and class-group that
apparently releases some of the coupling of classes involving virtual
types [Bru98, BV99].
In particular, an inherited group may have more components than the
parent group.
In fact, since LOOM already has
structural types, it would suffice to allow the definition of multiple match
bounds,
where all bounded type variables, as well as MyType, may occur in the
bounds. For instance, one should be able to write the following class type
in LOOM:
|
class (b <# á ñ,
a <# á m : MyType ® b ® unitñ
) |
[ u : a; m1 : a ® unit; m2 : b ® unit] |
|
Moreover, using recursive classes would allow one to use the type name of
one class to bound the type parameter of the other. Here is a possible
implementation of the classes subject
and observer
in LOOM extended with
recursively defined bounds and recursive classes:
class subject (E, O <# observer (E, MyType))
var observers : list(O) = []
methods
procedure add_observer (obs : O) { observers := obs :: observers }
procedure notify (e : E)
{ List.iter (function (obs:O) { obs.at_notification (self, e) }, observers) }
and observer (E, S <# subject (E, MyType))
methods procedure at_notification (s : S, e : E) { }
end class
A significant difference with [BOW98] is that grouping is not enforced here;
on the opposite, it remains optional, and may be used only when
convenient. Moreover, grouping here is just a standard recursive definition
and each definition can still be understood (and used) independently from the
other.
In fact, the proposal for virtual types that is presented in [BOW98] is based on matching, as done in LOOM,
and is thus very close to the above schema, with the exception that it lacks
structural types. This exemplifies that the choice between structural
and declared object types is a rather significant design choice.
Extending type destructors to object types |
|
Yet another possibility is to use type destructors, which have recently been
proposed by Hofmann and Pierce [HP98]. Type
destructors retain some of the expressiveness of type operators provided by
F<:w while keeping within second order types. In fact, row variables
can be seen as a special form of type destructors for records where only the
expansion is retained (the contraction for record types has been formally
studied in [Rém90, Sag95], but it revealed to be useless
in practice, and therefore it has not been included in the language Ocaml).
Hence, type destructors might solve virtual types as well as row variables do
in Ocaml. However, the meta-theory of type destructors remains quite
complicated as a result of their generality, and record-type destructors are
not considered in [HP98].
4 |
Expressiveness and abstraction |
|
Examples of virtual types were first motivated by the need for
expressiveness so that safe practical examples would not be rejected. The
first solutions to virtual types were unsafe. Safe solutions have been
proposed in [BOW98, Tor98].
In both works, the priority is given to expressiveness, i.e.
to the ability to write known useful examples in a sound typed language.
Recently, virtual types have also been studied for their
abstraction capabilities [IP99].
The cow/animal example, introduced by David
Shang [Sha96], actually mixes both issues; it also
demonstrates the use
of virtual types for specializing the type of a class by inheritance.
First, we present our solution in Ocaml; then, we discuss the
issue of type abstraction.
In its simplest and naive form, this example does not require mutual
recursion between open object types; it could a priori be typed in
languages based on F<: with parametric classes.
The example straightforwardly translates into Ocaml, as virtual types are
here only used to provide bounded type parameterization of classes.
We first define a hierarchy of food classes.
class food = object method food = () end ;;
class grass = object inherit food method grass = () end;;
class meat = object inherit food method meat = () end;;
We then introduce a generic class for starving animals.
class virtual ['a] starving_animal f =
object
constraint 'a = #food
method diet : 'a = f
method virtual private eat : 'a -> unit
end ;;
The method eat
is flagged virtual
because it is not yet
implemented. The class must also be flagged virtual
, so that no
instance of the class can be taken. The method eat
is flagged
private
so that it does not appear in the type of starving animals,
which is <diet : 'a>
where 'a
is the type of the food it eats.
One can also define the class type of real animals (with a method eat):
class type ['a] animal =
object constraint 'a = #food method diet : 'a method eat : 'a -> unit end;;
Of course, an animal is not an omnivore. The former eats some not yet
specified food, while the latter eats any kind of food.
class omnivore =
object inherit [food] starving_animal (new food) method eat f = f#food end;;
Note that the type omnivore
is equal to food animal
.
We can define a particular race of animals that eat a specific type of
food.
class camel = object
inherit [grass] starving_animal (new grass) method eat (f:grass) = f#grass
end;;
The important property is to statically ensure that a camel can eat grass,
but not meat (the phrase in italic is ill-typed).
new camel#eat (new grass);;
new camel#eat (new meat);;
Using an explicit coercion would not help, since meat
is not a subtype of
grass
. A camel cannot either eat some unspecified kind of food, that is
new camel#eat (new food)
is also
ill-typed.
It is important to be able to forget the exact diet of camels. Of
course, a camel is not an omnivore since it cannot eat any type of food.
To hide information about the type of food by subtyping, one must also
hide method eat
, which is contravariant in the type food, so as to be
sound.
Indeed, an animal cannot be fed if we do not know its diet.
A camel can still be seen as a grass starving_animal
since the food a camel eats is grass
.
A camel is also a food starving_animal
:
the food a camel eats is food
, since grass
is (a subtype of) food
.
Conversely, a camel is not a meat animal
, since grass
is not (a subtype
of) meat
.
In [IP99],
an animal is given the following type:
|
animal =def
á t = $ a <: food.a; diet : t; eat : t ® unitñ |
|
This says that an animal is an object with two methods diet and eat of
respective types t and t ® unit for some type t that is a subtype
of food. A camel and an omnivore have similar types, but with t
known exactly:
|
camel : á t = grass; diet : t; eat : t ® unitñ |
omnivore : á t = food; diet : t; eat : t ® unitñ |
|
By subtyping, a camel and an omnivore can both be given the type animal,
forgetting their exact type, but retaining the fact that they eat some kind
of food. Both of them can also be given the type of starving animals
á t = $ a <: food.a; diet : tñ, now ignoring that
they can eat.
Partially abstract object types are obviously more precise than our object
types. In particular, we are not able to express the type animal and we
can only give camels the weaker type of starving animals. Practically,
however, the difference is not very significant: an object of type animal
cannot eat: since its diet is unknown, whatever food it is offered will be
rejected. Thus, we have not really lost expressiveness here, but solely
some form of abstraction. Conversely, common examples of the form of
subject/observer pattern that involve mutual recursion of object types
cannot be typed in [IP99].
In [BV99] Bruce and Vanderwaart add
hash-types to provide a form of partially abstract type that is quite
similar to [IP99] in terms of expressiveness.
Partially abstract types are an orthogonal feature that can also be added to
mostly any language (even if this may not always be immediate and may
sometimes raise other design issues). This confirms our early claim
that virtual types are not an atomic construction, since it simultaneously
contains a form of universal and existential (bounded) quantification.
5 |
At the limit of virtual types |
|
Alternating lists have been used in [BOW98] as the basic example to illustrate the
strength of virtual types. In this section, we reuse alternating lists, but
as a counter-example to virtual types.
In fact, alternating lists have been chosen as a simple example of recursively
define classes, and in particular, as a simplification of recursively
defined parse trees [PS94]4. We argue that
alternating lists are much better implemented by parametric types than by
virtual types. Using parametric polymorphism, we show that there is a
continuous sequence of refinements starting with very simple classes and
leading to alternating lists. Conversely, virtual types can only address a
particular point in this sequence.
In Lisp the most important data structure is the memory cell called a
cons
. In an object oriented setting, it can be defined as follows:
class ['a, 'b] cons h t = object
val head : 'a = h val tail : 'b = t
method null = false method car = head method cdr = tail
end;;
A cons can be used as a pair, that is, a cell filled with two
values of different types.
In Lisp, one uses a special pointer, usually called nil
, to
distinguish from conses. Actually, it is convenient to give the class
nil
the same interface as the class cons
since nil
is
typically used to end a chain of conses in a binary tree or a list.
exception Null;;
class ['a, 'b] nil = object
method null = true method car : 'a = raise Null method cdr : 'b = raise Null
end;;
With parametric classes, one can define heterogeneous lists, whose
elements can be of different types.
In many situations one need only consider
homogeneous lists; hence we can define the more precise type:
type 'a homo_list = ('a, 'a home_list) cons;;
Alternating lists are just another particular instance of conses that could be
defined as follows:
type ('a, 'b) alt_list = ('a, ('b, ('a, 'b) alt_list) cons) cons;;
Indeed, alternating lists have nothing to do with virtual types. Here, we
have only specialized the types of classes cons
and nil
leaving the code unchanged.
The type constraint can be enforced in subclasses of
cons
and nil
rather than in
objects of those classes by restricting the types of the class parameters
(we only show the subclass for cons):
class ['a, 'alt_self] alt_cons h t =
object (_ : 'self)
constraint 'alt_self = ('b, 'self) #cons
inherit ['a, 'alt_self] cons h t
end;;
Alternating lists are not final classes, and can be specialized using
inheritance. For instance, it is straightforward to add a method
returning the length of the list.
Of course, many more variations ---including side effects and binary
methods--- can be devised, as for any class, just
playing with parameterization and inheritance.
In summary, alternating lists appear as one particular point in a sequence of
successive refinements of parametric classes. All examples can be programmed
naturally and uniformly with parametric classes. On the opposite, virtual
types would show up abruptly in the middle of the sequence. We do not see
any problem in extending our treatment of alternating lists to other more
complicated but similar patterns, such as parse trees, even though we are
not convinced that parse trees are a good use of objects.
We have shown that several practical examples presented as a justification for
the special treatment of virtual types can in fact be directly programmed in
Ocaml. We achieved this result by decoupling related classes that had to be
recursively defined, previously. Decoupling is made possible in Ocaml by
the use of row variables. A few other languages that have both binary
methods and structural types can also solve virtual types with parametric
polymorphism, as easily.
Grouping can still be allowed, but as a notational convenience rather than
as a fundamental and new construction. Given the simplicity and the
additional flexibility that we have obtained by decoupling classes, we claim
that parametric polymorphism and structural types are preferable to virtual
types.
Indeed, parametric polymorphism is well-established,
easy-to-manipulate and expressive. Its strength is hereby reaffirmed.
Structural types are also commonly used in type theory and almost exclusively
used in higher-order type systems. However, declared types are still common
among programming languages with weaker, usually non parametric, type
systems.
Our experience with virtual types certainly reinforces the importance of
structural types. This may be an issue to consider seriously in the design
of type systems for new programming languages.
[BCC+96]-
Kim B. Bruce, Luca Cardelli, Giuseppe Castagna, the Hopkins Objects
Group (Jonathan Eifrig, Scott Smith, Valery Trifonov), Gary T.
Leavens, and Benjamin Pierce.
On binary methods.
Theory and Practice of Object Systems, 1(3):221--242, 1996.
- [BFP97]
-
Kim B. Bruce, Adrian Fiech, and Leaf Petersen.
Subtyping is not a good ``match'' for object-oriented languages.
In ECOOP, number 1241 in LNCS, pages 104--127. Springer-Verlag,
1997.
- [BOW98]
-
Kim B. Bruce, Martin Odersky, and Philip Wadler.
A statically safe alternative to virtual types.
In Informal proceedings of the FOOL'5 workshop, 1998.
Available electronically at http://pauillac.inria.fr/~remy/fool/.
- [Bru95]
-
Kim B. Bruce.
Typing in object-oriented languages: Achieving expressibility and
safety.
Revised version to appear in Computing Surveys, November 1995.
- [Bru97]
-
Kim B. Bruce.
Increasing java's expressiveness with ThisType and match bounded
polymorphism.
Technical report, Williams College, 1997.
- [Bru98]
-
Kim B. Bruce.
Do parametric types beat virtual types?
Private Email discussion, October 1998.
- [BSvG95]
-
Kim B. Bruce, Angela Schuett, and Robert van Gent.
Polytoil: A type-safe polymorphic object-oriented language.
In ECOOP, number 952 in LNCS, pages 27--51. Springer Verlag,
1995.
- [BV99]
-
Kim B. Bruce and Joseph C. Vanderwaart.
Sematics-driven language design: Type-safe virtual types in
object-oriented languages.
Available electronically at http://www.cs.williams.edu/~kim/,
February 1999.
- [Car90]
-
Luca Cardelli.
Notes about F<:w.
Unpublished notes, October 1990.
- [CCH+89]
-
Peter Canning, William Cook, Walter Hill, Walter Olthoff, and John Mitchell.
F-bounded quantification for object-oriented programming.
In Fourth International Conference on Functional Programming
Languages and Computer Architecture, pages 273--280, September 1989.
- [EST95a]
-
J. Eifrig, S. Smith, and V. Trifonov.
Sound polymorphic type inference for objects.
In OOPSLA, 1995.
- [EST95b]
-
J. Eifrig, S. Smith, and V. Trifonov.
Type inference for recursively constrained types and its application
to OOP.
In Mathematical Foundations of Programming Semantics, 1995.
- [GHJV95]
-
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.
Design Patterns.
Addison-Wesley, 1995.
- [HP98]
-
Martin Hofmann and Benjamin C. Pierce.
Type destructors.
In Informal proceedings of the FOOL'5 workshop, 1998.
Available electronically at http://pauillac.inria.fr/~remy/fool/.
- [IP99]
-
Atsushi Igarashi and Benjamin C. Pierce.
Foundations for virtual types.
In Informal proceedings of the FOOL'6 workshop, 1999.
Available electronically at http://www.cs.williams.edu/~kim/FOOL/FOOL6.html.
- [KMMPN87]
-
Bent Bruun Kristensen, Ole Lehrmann Madsen, Birger Moller-Pedersen, and Kristen
Nygaard.
The BETA programming language.
In Bruce Shriver and Peter Wegner, editors, Research Directions
in Object-Oriented Programming, pages 7--48. The MIT Press, Cambridge, Mass,
1987.
- [Ler96]
-
Xavier Leroy.
The Objective Caml system (version 2.00).
Software and documentation available on the Web,
http://caml.inria.fr/, 1996.
- [OW97a]
-
Martin Odersky and Philip Wadler.
Pizza into java: Translating theory into practice.
In Proc. 24th symposium Principles of Programming Languages,
pages 146--159. ACM Press, January 1997.
- [OW97b]
-
Martin Odersky and Philip Wadler.
Pizza into java: Translating theory into practice.
In 24'th ACM Symposium on Principles of Programming Languages,
Paris, January 1997.
- [PS94]
-
Jens Palsberg and Michael I. Schwartzback.
Object Oriented Type Systems.
Wiley, 1994.
- [Rém90]
-
Didier Rémy.
Algèbres Touffues. Application au Typage Polymorphe des
Objets Enregistrements dans les Langages Fonctionnels.
Thèse de doctorat, Université de Paris 7, 1990.
- [Rém94]
-
Didier Rémy.
Type inference for records in a natural extension of ML.
In Carl A. Gunter and John C. Mitchell, editors, Theoretical
Aspects Of Object-Oriented Programming. Types, Semantics and Language
Design. MIT Press, 1994.
- [RV98]
-
Didier Rémy and Jérôme Vouillon.
Objective ML: An effective object-oriented extension to ML.
Theoretical And Practice of Objects Systems, To appear, 1998.
A preliminary version appeared in the proceedings of the 24th ACM
Conference on Principles of Programming Languages, 1997.
- [Sag95]
-
Camille le Moniès de Sagazan.
Un système de types étiquetés polymorphes pour typer les
calculs de processus à liaisons noms-canaux dynamiques.
Thèse de doctorat, Université Paul Sabatier, Toulouse,
November 1995.
(Also CNRS/LAAS report 95077).
- [Sha96]
-
David Shang.
Is a cow an animal ?
Unpublished note, January 1996.
- [SP94]
-
Martin Steffen and Benjamin C. Pierce.
Higher-order subtyping.
Technical report ECS-LFCS-94-280, University of Edinburgh, February
1994.
- [Tor98]
-
Mads Torgersen.
Virtual types are statically safe.
In Informal proceedings of the FOOL'5 workshop, 1998.
Available electronically at http://pauillac.inria.fr/~remy/fool/.
- [Wad98]
-
Philip Wadler.
Do parametric types beat virtual types?
Private Email discussion, October 1998.
- [Wan87]
-
Mitchell Wand.
Complete type inference for simple objects.
In D. Gries, editor, Second Symposium on Logic In Computer
Science, pages 207--276, Ithaca, New York, June 1987. IEEE Computer
Society Press.
A |
A more natural and flexible solution to the subject/observer pattern |
|
The general idea behind the subject/observer pattern is to realize a complex
operation by the tight collaboration of several objects of different
classes. Here, the operations are implemented in the subject while all the
control remains in the observer.
In this regards, the use of type event
to communicate between the subject
and the observer is surprising and not so modular: typically, any refinement
of the communication pattern will imply a simultaneous refinement of the
event
class used to communicate between the subject and the observer.
Actually, the event
class is used to pack in an extensible sum type
messages that are then pattern-matched and treated by the observer class.
This weakens the security, since the system does not check for exhaustive
pattern-matching.
A first solution in Ocaml is to represent events as a variant
instead of as an object. This is described in appendix 1.
Yet, a new, direct, and safer implementation of the subject/observer pattern
would let the subject notify the observer by calling the appropriate message
of the observer; this is described in section A.2.
A.1 |
Representing events in a variant type |
|
In is part we show a slight improvement of the example given in section 1 that represent actions as variants.
The root classes subject
and observer
are left unchanged.
The class window
is then using a variant `Move
to signal a
move.
class ['O, 'E] window =
object (self)
inherit ['O, 'E] subject
val mutable position = 0
method move d = position <- position + d; self#notify `Move
method draw = Printf.printf "{Position = %d}" position;
end;;
class ['a, 'b] window :
object ('c)
constraint 'a = < at_notification : 'c -> 'b -> unit; .. >
constraint 'b = [> `Move]
val mutable observers : 'a list
val mutable position : int
method add_observer : 'a -> unit
method draw : unit
method move : int -> unit
method notify : 'b -> unit
end
Note that, since variants are extendible, the role of the function
event
coercing actions to the type of events is played by the
primitive operations on variants.
Correspondingly, the class manager
is changed to:
class ['S, 'E] manager =
object
inherit ['S, 'E] observer
method at_notification s e = match e with `Move -> s#draw | _ -> ()
end;;
class ['a, 'b] manager :
object
constraint 'a = < draw : unit; .. >
constraint 'b = [> `Move]
method at_notification : 'a -> 'b -> unit
end
Note the _
that allows at_notification
to be called with
different tags, in which case the default behavior is to ignore the
message. This is reminded in the type constraint 'b = [> `Move]
,
which says that the variant type 'b
can have tag `Move
with
a value of type unit, or any other tag with a value of any type.
A refined version of windows
simply use other tags as needed:
class ['O, 'E] big_window =
object (self)
inherit ['O, 'E] window
val mutable size = 1
method resize x = size <- size + x; self#notify `Move
val mutable top = false
method raise = top <- true; self#notify (`Resize false)
method draw = Printf.printf "{Position = %d; Size = %d}" position size;
end;;
Similarly, the refined version of the manager
may respond to the new
tags:
class ['S, 'E] big_manager =
object
inherit ['S, 'E] manager as super
method at_notification s e =
match e with `Resize b -> s#raise | _ -> super#at_notification s e
end;;
Here a test showing that classes can correctly be combined:
(We assume an obvious redefinition of class trace_observer
).
let w = new big_window in w#add_observer (new big_manager); w#resize 2; w#move 1;;
{Position = 0; Size = 3}{Position = 1; Size = 3}- : unit = ()
This new version of the subject/observer pattern is simpler that the
previous one, by avoiding the useless encoding of variants into objects.
However, it does not provide much more security. In particular, the
at_notification
method will accept all tags in prevision of further
extension. That is, a forgotten specialization of the method
at_notification
will not produce an typechecking error, but will
simply ignore the notification. Below is a solution that fixes this problem
and that is thus more secure. It is also simpler.
A.2 |
Using different methods for each kind of event |
|
.
The method notify
now takes a message to be send to all registered
observers.
class ['O] subject =
object (self : 'mytype)
val mutable observers : 'O list = []
method add_observer obs = observers <- obs :: observers
method notify (message : 'O -> 'mytype -> unit) =
List.iter (fun obs -> message obs self) observers
end;;
class ['a] subject :
object ('b)
val mutable observers : 'a list
method add_observer : 'a -> unit
method notify : ('a -> 'b -> unit) -> unit
end
The class observer
is initially empty.
class ['S] observer = object end;;
A window/manager is obtained by inheriting form the above pattern. For
instance, the class window may have a method move
whose code will in turn
notify all observers with a messaged moved
. Correspondingly, the
window-manager is extended to accept messages moved
: it then simply calls
back the method draw
of the window that signaled the move (called the
moved
message of the manager):
class ['O] window =
object (self : 'mytype)
inherit ['O] subject
val mutable position = 0
method move d = position <- position + d; self#notify (fun x -> x#moved)
method draw = Printf.printf "[Position = %d]" position;
end;;
class ['S] manager =
object
inherit ['S] observer
method moved (s : 'S) : unit = s#draw
end;;
An instance of this pattern is well-typed because the manager correctly
treats all messages sent by the window.
let w = new window in w#add_observer (new manager); w#move 1;;
[Position = 1]- : unit = ()
This would not be the case if, for instance, we forgot to implement the
moved
message in class manager
.
The pattern can further be extended.
Instead of extending the event
class, as in
section 1 one can more appropriately notify the
observers on another method ("resized" in the example below):
class ['O] big_window =
object (self)
inherit ['O] window as super
val mutable size = 1
method resize x = size <- size + x; self#notify (fun x -> x#resized true)
method draw = super#draw; Printf.printf "[Size = %d]" size;
end;;
class ['S] big_manager =
object
inherit ['S] manager as super
method resized b (s:'S) = if b then s#draw
end;;
To check the flexibility, we also add a trace_observer
that is a
refinement of the class observer
:
class ['S] trace_observer =
object
inherit ['S] observer
method resized (b:bool) (s:'S) = print_string "<R>"
method moved (s:'S) = print_string "<M>"
end;;
let w = new big_window in
w#add_observer (new big_manager); w#add_observer (new trace_observer);
w#resize 2; w#move 1;;
<R>[Position = 0][Size = 3]<M>[Position = 1][Size = 3]- : unit = ()
- 1
- INRIA Rocquencourt, BP 105, 78153 Le Chesnay Cedex France.
- 2
- This
parameterization is suggested but not implemented in [BOW98].
- 3
- In
[BCC+96], object are directly represented as records
of functions.
- 4
- Recursively
defined parse trees were chosen as a concise illustration rather than as a
practical example in [PS94].
This document was translated from LATEX by
HEVEA.