** 22. Bounded Polymorphism ** ---------------------------------------------------------------------------- 22.1. When we have subtyping in our language, we can extend parameteric polymorphism to be bounded. This allows us to place constraints on the types that are instances, allowing us to perform interesting operations on values of the type parameter. E.g. say we have a hierarchy of collection classes, rooted at Collection (which is parameterized by T, the type of the elements): class Collection[T] { abstract method do(f:T->unit):unit } Now we want to define a print operation over all collections whose elements can be printed. First, we declare the type of printable things: type Printable { abstract method print():unit } Then we define the print function: fun print(c:Collection[T <= Printable]):unit { c.do(fn x => x.print()) } This function takes an argument of type Collection[T <= Printable]. Written in more CoreML-like notation, this would be forall T where T <= Printable. Collection[T] We've added the ability to place a constraint on the type parameter T, namely that it is a subtype of Printable. We will use subtype bounds like this all over. For example, for a Set[T] datatype, we will require that T be Comparable (i.e., support the equals method). For a HashTable, we will require that the keys be both Comparable and Hashable. And so on. This bounded polymorphism is critical for code reuse: we can write a single piece of code (e.g. the print function above) and reuse it for all argument types that meet the constraints. In ML, we can only write code that either doesn't care at all about the argument, or knows exactly what it is. Bounded polymorphism gives us a very important middle ground. ---------------------------------------------------------------------------- 22.2. We might like the print function to instead be a print method of Collection. If we do that, the c parameter becomes the implicit receiver. But where does the T <= Printable constraint go? One solution is to allow methods to have "where" clauses that give them a way to place additional constraints on self's type parameters. E.g.: class Collection[T] { abstract method do(f:T->unit):unit method print():unit where T <= Printable { do(fn x => x.print()) } } In this case, the print function is not defined on all Collections, but only on those whose element types are Printable. We'd also like to know that Collections of Printable things are themselves Printable. How to indicate this conditional subtyping? Here's one possible notation: class Collection[T] (extends Printable when T <= Printable) { ... } Cecil puts all methods outside of classes, making the original function definition of print the only way to write things (no special treatment is needed for self vs. any other argument). Cecil also allows subtyping links to be declared separately from classes, and these links can have type constraints. So in Cecil-like syntax, the code might look like: type Printable; signature print(Printable):unit; class Collection[T]; abstract method do(c:Collection[T], f:T->unit):unit; extend Collection[T <= Printable] subtypes Printable; method print(c:Collection[T <= Printable]):unit { c.do(fn x => x.print()) } ---------------------------------------------------------------------------- 22.3. Comparable (the type of things that can be compared for equality) is a common bounding type. It's what ML's built-in equality type parameters give (except that they support only the built-in implementation of equality, not arbitrary user-defined equality methods). But it's actually tricky to define. Let's try. Here's a likely partial definition of Sets: class Set[T <= Comparable] extends Collection[T] { method member(x:T):bool { do(fn x' => if x.equals(x') then return true); return false; } ... } Here's what Comparable might be defined as: type Comparable { abstract method equals(arg:Comparable):bool; } Here's what an implementation of Comparable might be defined as: class Int implements Comparable { ... method equals(x:Int):bool { ... code to compare integers ... } } This implementation of Int doesn't work, though, because we didn't follow the contravariant rule when we overrode Comparable's equals interface with one with a more precise argument type. How to fix this? 1) Force Int's equals to take any Comparable: class Int implements Comparable { ... method equals(x:Comparable):bool { ... } } This fixes the type error, but now it's really hard to implement Int's equals method; one has to handle any Comparable value. Maybe this isn't too hard for equals (if it isn't an int, return false), but it will get to be a bigger burden when we define the Ordered interface which supports less_than etc. operations. 2) Change the Comparable interface definition to restrict the argument type to be the same as the receiver type. Some theoretical languages have introduced SelfType as a special type to mean exactly this. type Comparable { abstract method equals(arg:SelfType):bool; } Now the Int equals method is OK as originally written, but the member function won't necessarily typecheck, since we don't know that x and x' are the exact same type, only that they have some common supertype T. 3) Similar to 2, except parameterize Comparable by the type of things that can be compared against: type Comparable[T] { abstract method equals(arg:T):bool; } We can declare precisely the kind of things that Ints can be compared against, namely other Ints (or subtypes of Int): class Int implements Comparable[Int] { ... method equals(x:Int):bool { ... } } And we can say that the elements of Sets should be comparable to each other: class Set[T <= Comparable[T]] extends Collection[T] { method member(x:T):bool { do(fn x' => if x.equals(x') then return true); return false; } ... } This all works, as long as we can make sense of the seemingly recursive type bound "T <= Comparable[T]". This kind of bound, where the lhs type appears in the rhs type somewhere, is called "F-bounded polymorphism" (it was originally written as t <= F(t), where F was any function from a type to a type in the typing language). Fortunately, this seemingly recursive bound isn't really recursive. To check whether a type tau has this bound, you first bind T to tau, and then compute F(tau), and then check whether tau <= F(tau). To typecheck code within the polymorphic definition (e.g. the body of the member function), you first bind T to some fresh dummy type tau, then compute F(tau), then enter into your subtyping environment that it's safe to assume that tau <= F(tau), then typecheck code under this environment. This is made somewhat clearer by the Core ML version of the constraint: forall T where T <= Comparable[T]. Set[T] The T is bound, and then the constraint is checked against two type expressions, both of which happen to involve T. Bottom line: F-bounded polymorphism, not just plain bounded polymorphism, is needed for practical bounded parameteric polymorphism in OO languages. Current proposals for bounded polymorphism for Java (e.g. GJ) include F-bounded polymorphism. ---------------------------------------------------------------------------- 22.4. An alternative to subtype-bounded polymophism is signature-bounded polymorphism. In this regime, one just lists the signatures that are required to be supported over type parameters, without creating a separate type like Printable or Comparable to represent it. Our Collection and Set examples can be written using signature-bounded polymorphism as follows: class Collection[T] { abstract method do(f:T->unit):unit method print():unit where T has print():unit { do(fn x => x.print()) } } class Set[T] extends Collection[T] where T has equals(T):bool { method member(x:T):bool { do(fn x' => if x.equals(x') then return true); return false; } ... } class Int { ... method equals(x:Int):bool { ... } } In general, a polymorphic class or method can be given a where clause, whose body is a sequence of "T has op(args):res, ..." declarations listing operations that need to be supported by things of type T. F-bounded polymorphism goes away by allowing signatures like "T has equals(T):bool", where any of the type parameters can appear in the types of any of the operations. But this is natural, since the type parameters are in scope already. Signature-bounded polymorphism has a lot going for it, including simplicity. It also doesn't require types like Printable and Comparable to be defined, although it still might be nice to give names to these notions.