CSE341 -- Notes on Pizza

Pizza is a Java extension that incorporates:

Pizza is implemented as a pre-processor. Two different translation schemes:

In the rest of these notes, we'll just look at parametric polymorphism (including some background information on subtyping rules).

Motivation

Java already has bounded polymorphism for the types of variables that hold objects. If a variable v has type Ball, then v can hold any object of type Ball or of a subtype of Ball, e.g. ColoredBall. If we ignore the primitive types, Java also in effect supports universal polymorphism, since we can declare a variable to be of type Object.

We had problems, though, when we wanted to have things like a HashSet of Points -- unless we implement a different class of PointHashSet, all Java's type system will tell us is that the HashSet holds Objects.

Simple example of polymorphism

from the paper:
class Pair<elem> {
  elem x; elem y;
  Pair (elem x, elem y) {this.x = x; this.y = y;} 
  void swap () {elem t = x; x = y; y = t;}
}

Pair<String> p = new Pair("world!", "Hello,");
p.swap();
System.out.println(p.x + p.y);

Pair<int> q = new Pair(22,64);
q.swap;
System.out.println(q.x - q.y);
Suppose we tried to write this in ordinary Java:
class Pair {
  Object x; Object y;  /* won't work with primitive types */
  Pair (Object x, Object y) {this.x = x; this.y = y;} 
  void swap () {Object t = x; x = y; y = t;}
}

Pair p = new Pair("world!", "Hello,");
p.swap();
/* this doesn't work - lost the type info */
System.out.println(p.x + p.y);  
/* do this instead:  */
System.out.println((String)p.x + (String)p.y);


/* this doesn't work because 22 is an int, which is a primitive type */
Pair q = new Pair(22,64);
q.swap;
System.out.println(q.x - q.y);  /* TYPE ERROR HERE */

Miranda Analog

represent a pair as a tuple: ("world,"Hello,")
swap:: (*,*)->(*,*)
swap (x,y) = (y,x)

p = ("world,"Hello,")
ps = swap p

Heterogenous translation of Pair

class Pair_String {
  String x; String y;
  Pair_String (String x, String y) {this.x = x; this.y = y;} 
  void swap () {String t = x; x = y; y = t;}
}

class Pair_int {
  int x; int y;
  Pair_int (int x, int y) {this.x = x; this.y = y;} 
  void swap () {int t = x; x = y; y = t;}
}

Pair_String p = new Pair_String("world!", "Hello,");
p.swap();
System.out.println(p.x + p.y);

Pair_int q = new Pair_int(22,64);
q.swap;
System.out.println(q.x - q.y);

Homogeneous translation of Pair

class Pair {
  Object x; Object y;
  Pair (Object x, Object y) {this.x = x; this.y = y;} 
  void swap () {Object t = x; x = y; y = t;}
}

Pair p = new Pair((Object)"world!", (Object)"Hello,");
/* above casts are not actually needed */
p.swap();
System.out.println((String)p.x + (String)p.y);

Pair q = new Pair((Object) new Integer(22), (Object) new Integer(64));
q.swap;
System.out.println(((Integer)(q.x)).intValue() - ((Integer)(q.y)).intValue();)

Bounded parametric polymorphism - simple example

interface Pretty {
  String prettyprint();
}

class Pair<elem implements Pretty> {
  elem x; elem y;
  Pair (elem x, elem y) {this.x = x; this.y = y;} 
  String printboth () {return x.pretty() + y.pretty()};
}
If we just declared the fields x and y to be of type Pretty then that's all we would know about the type of p.x -- but using bounded parameteric polymorphism we can give variables more specific types, such as Pair<SchemeCode> (where SchemeCode is a class that implements the Pretty interface).

Bounded parametric polymorphism and binary operations

It turns out that it is rather tricky to express precisely in an object-oriented language the type of a method in which the type of an argument is the same as the type of the receiver, or binary operations where both arguments are of the same type. This problem was first satisfactorily solved in a 1989 paper by Canning, Cook, Hill, Olthoff, and Mitchell, in which they introduced F-bounded polymorphic types. In F-bounded polymorphism, a name appears both as a bounded variable and in the bound on that variable.
/* Ord is an interface for objects that can be ordered.  It declares a
   less method that is used to compare two objects.  However, we only want
   to compare OrdInts with OrdInts and OrdStrings with OrdStrings, but 
   not OrdInts with OrdStrings. */

interface Ord<elem> {
  boolean less(elem o);
}

class Pair<elem implements Ord<elem>> {
  elem x; elem y;
  Pair (elem x, elem y) {this.x = x; this.y = y;} 
  elem min() {if (x.less(y)) return x; else return y;}
}

class OrdInt implements Ord<OrdInt> {
  int i;
  OrdInt (int i} {this.i = i;}
  int intValue() {return i;}
  public boolean less(OrdInt o) {return i < o.intValue()}
}

Pair<OrdInt> p = new Pair( new OrdInt(22), new OrdInt(64));
System.out.println(p.min().intValue());

Some variations that don't work

Gee ... isn't that class Pair<elem implements Ord<elem>> business more complex than necessary???
/* this version allows OrdInts to be compared with OrdStrings */

interface Ord {
  boolean less(Ord o);
}

class OrdInt implements Ord {
  int i;
  OrdInt (int i} {this.i = i;}
  int intValue() {return i;}
  public boolean less(Ord o) {return i < o.intValue()}
}

And in this version, OrdInt doesn't implement the Ord interface (according to Java's type rules).

  ...
  public boolean less(OrdInt o) {return i < o.intValue()}
  ...
Ha! You say. The problem is obviously that the stupid Java designers made an overly restricted rule about types for methods. Well, no. If the above version of OrdInt was allowed to implement the Ord interface, then Java's type system would be unsound. Consider:
Ord x = new OrdString("octopus");
Ord y = new OrdInt(42);
boolean b = x.less(y);

Covariant, Contravariant, and Equivariant Typing

These are are various alternative rules for deciding when one object-oriented type is a subtype of another type. Java uses the equivariant rule (sometimes called invariant rule). Contravariance is more general but unintuitive. Covariance is intuitive but alas unsound. (The above example with the octopus and 42 would be legal under covariant typing.) The covaraint rule is used by Eiffel, and by Java for the special case of arrays. Actually Java is still type safe, even for arrays, but has to insert runtime type checks.

Type checking for Java arrays

In Java String is a subtype of Object, and String[] is a subtype of Object[]. But, surprisingly, we can't actually use an array of strings everywhere an array of objects is expected!

String[] s = {"squid","clam"};
Object[] x;
x = s;  /* legal since String[] is a subtype of Object[] */
x[0] = new Beachball(); /* we get a runtime exception here */

/* if we had allowed the beachball to get into our array of strings, 
    the following statement would be using + for beachballs: */
System.out.println(s[0]+s[1]);

Covariant vs. Contravariant vs. Equivariant Rule

How do we decide when an object of a given type S can be used when an object of type T is expected?

The contravariant rule for subtyping is used in Emerald, Cecil, Trellis/Owl, etc. (It is also the rule used in Cardelli's paper "A Semantics of Multiple Inheritance".)

The covariant rule is used in Eiffel, and in Java for arrays (as a special case).

The equivariant rule is used in Java for everything but arrays.

Contravariant rule:

S is a subtype of T if:

  1. S provides all the operations that T does (and maybe some more)
  2. For each operation in T, the corresponding operation in S has the same number of arguments and results
  3. The types of the results of S's operations are subtypes of the types of the corresponding results of T's operations
  4. The types of the arguments of T's operations are subtypes of the types of the corresponding arguments of S's operations (note the reversal of T and S here)
N.B. If S=T, then S is a subtype of T.

Covariant rule:

same as above, except for 4:

  1. The types of the arguments of S's operations are subtypes of the types of the corresponding arguments of T's operations

Equivariant rule:

S is a subtype of T if:

  1. S provides all the operations that T does (and maybe some more)
  2. For each operation in T, the corresponding operation in S has the same number of arguments and results
  3. The types of the results of S's operations are the same as the types of the corresponding results of T's operations
  4. The types of the arguments of T's operations are the same as the types of the corresponding arguments of S's operations
Note that in Java, if we have two operations with the the same name and number of arguments, but different types of the arguments, Java views these as two different operations (overloading), rather than as a conflict.

Java - Array Analog

Here are ordinary Java classes that are analogous to Object[] and String[]
class ObjectArray {
  Object get(int i) { ...}
  set(int i, Object x) { ...}
}

class StringArray {
  String get(int i) { ...}
  set(int i, String x) { ...}
}
For arrays, Java behaves as if StringArray is a subtype of ObjectArray - but this is not sound. Consider the analog of the squid/beachball example:
StringArray s = new StringArray();
ObjectArray x;
x = s;
s.put(0,"squid");
s.put(0,"clam");

x.put(0,new Beachball());  /* BAD!! */

/* the following statement is using + for beachballs: */
System.out.println(s.get(0) + s.get(1));

Other Java Examples

    class Point {
      public int x;
      public int y;
    }

    class ColoredPoint extends Point {
      public Color mycolor;
    }


    interface PointMaker {
      Point makePoint();
    }

    interface PointEater {
      void eat(Point p);
    }

    interface ColoredPointMaker {
      ColoredPoint makePoint();
    }

    interface ColoredPointEater {
      void eat(ColoredPoint p);
    }
The following alternate declaration for ColoredPointMaker would be illegal:
    interface ColoredPointMaker extends PointMaker {
      ColoredPoint makePoint();
    }
The following alternate declaration for ColoredPointEater is OK, and says that a ColoredPointEater must provide two different eat methods:
    interface ColoredPointEater extends PointEater {
      void eat(ColoredPoint p);
    }
Both these declarations are legal:
    class PtMaker1 implements PointMaker {
      Point makePoint() {
	 return new Point();
      }
    }
    class PtMaker2 implements PointMaker {
      Point makePoint() {
	 return new ColoredPoint();
      }
    }
The following is illegal under Java's equivariant type rule. (It would be legal under the covariant and contravariant rules.)
    class PtMaker3 implements PointMaker {
      ColoredPoint makePoint() {
	 return new ColoredPoint();
      }
    }
The following is also illegal under Java's equivariant type rule. (It would be legal under the contravariant rule, but not the covariant rule. It would be type safe.)
    class Eater1 implements ColoredPointEater {
      void eat (Point p) {
      }
    }
Finally, the following is illegal under Java's equivariant type rule. (It would be legal under the covariant rule, but not the contravariant rule. It would not be type safe unless there were a runtime check.)
    class Eater2 implements PointEater {
      void eat (ColoredPoint p) {
      }
    }