The concept of subtyping is particularly critical in most object-oriented programming languages, but it is also useful in other settings.
We say that a type
t1is a subtype of another typet2, and we writet1 <: t2, if a value of typet1can be used anywhere where a value of typet2is expected.
A good example was our discussion of 2-dimensional points vs 3-dimensional points. Any operation that expects a 2-dimensional point will make use of the two coordinates x and y. But a 3-dimensional point also has those two coordinates. So should we allow these operations to be performed on 3-dimensional points as well? i.e. is point3d a subtype of point2d? This is a crucial question for object-oriented languages: Classes are often identified with types in that case, and a subclass relation must also correspond to a subtype relation, as the main point of subclasses is that their objects can be used wherever objects of the superclass could.
The above example points out that records are at a relevant topic of discussion for subtyping. We can consider a record type to be a subtype of another if:
It is an important question to decide what kinds of rules subtyping should obey. We can list some of these rules:
t <: t (reflexive property)t1 <: t2 and t2 <: t3 then t1 <: t3.{a: t1, b: t2, c: t3} <: {a: t1, b: t2}The presence of mutation poses some interesting problems with depth subtyping. We might want a rule that says:
t1 <: t2, then {a: t1, ...} <: {a: t2, ...} where all other fields are kept the same.But if we are allowed to mutate a record’s fields, we might have the following series of operations:
let change_center (c: {center: {x: x}, r: r}) =
c.center := {x: 0};;
let sph = {center= { x= 0, y= 0}, r= 3};;
change_center sph;;
sph.center.y;; <---------- Will get an error!
This is a series of operations the typechecker should allow:
change_center takes as input a value of type {center: {x: int}, r: int}. It replaces the value of center with a value of type {x: int}.sph starts off as a value of type {center: {x: int, y: int}, r: int}.sph would be a valid input to the change_center function.sph has type {center: {x: int, y: int}, r: int}, we should be allowed to access sph.center.y.And yet at the end of the day we end up accessing an entry that is not there! Our type system is no longer sound! This is the kind of error it was supposed to protect us from. So we cannot have mutable record fields and depth subtyping in a sound type system.
A similar problem arises in the use of (mutable) arrays in languages like Java. Consider for example the following code, borrowed from Dan Grossman’s course:
class Point { ... } // has fields double x, y
class ColorPoint extends Point { ... } // adds field String color
...
void m1(Point[] pt_arr) {
pt_arr[0] = new Point(3,4);
}
String m2(int x) {
ColorPoint[] cpt_arr = new ColorPoint[x];
for(int i=0; i < x; i++)
cpt_arr[i] = new ColorPoint(0,0,"green");
m1(cpt_arr);
return cpt_arr[0].color;
}
Let’s describe what is going on:
Point, and subclass ColorPoint. This also establishes a subtype relation.m1 that takes an array of Point objects, and changes the 0-th entry in that array to a new Point.m2 that takes an integer, then creates an array of that many ColorPoint instances.m1 that was expecting a Point array, as ColorPoint is a subtype of Point. This is essentially a form of depth subtyping.m1 changes the first entry in the array (to an instance of Point).Point, not ColorPoint, so we would get an error.Clearly something went very wrong. The question is what, and how to prevent it. What should we “fix”?
Java’s approach is as follows: When m1 runs, even though on a static typing level it expected a Point array, it has runtime information that it is indeed a ColorPoint array. So when m1 tries to put a Point instance in the first entry, there will be a runtime exception thrown because Point is not a subtype of ColorPoint and the array is actually a ColorPoint array.
So in essence, in order to allow both depth subtyping in the form of arrays and mutation, Java is required to insert some runtime checks instead of static checks to ensure that when a value is mutated then a value of the correct runtime “type” is inserted. But it does mean that our static type system is catching fewer errors.
If we have time we will discuss bounded polymorphism, which affords a way to control such problems at the static level.
In this section we address a simple question: When is a function type T1 -> S1 a subtype of another function type T2 -> S2?
For this we apply the essence of subtyping: We should be able to use the function f of type T1 -> S1 anywhere where a function of type T2 -> S2 is expected. Let us see what this means:
f as a function of type T2 -> S2.T2. But since f can only handle inputs of type T1, it must be the case that T2 <: T1.S1. But we must be able to think of it as a result of type S2. So it must be the case that S1 <: S2.So we have:
We have the function subtype relation
(T1 -> S1) <: (T2 -> S2)if and only ifT2 <: T1andS1 <: S2.
We say that function types are covariant in the return type but contravariant in the argument type.
Both generics (polymorphic types) and subtyping are desirable features, and while including them both in a language design is doable, it presents some questions, especially in relation to mutation.
As an example, suppose we have a structure for mutable lists, List<T>. And suppose we have two types, ColorPoint <: Point. Then we would like to write a function that “filters” those points that are within say a given distance from the origin. So a function:
closeEnough: List<Point> -> List<Point>
Suppose further that we have a list of ColorPoints:
List<ColorPoint> lst = [p1, p2, p3]
Then we would like to feed that into the closeEnough function:
lst2 = closeEnough lst
The first problem we encounter is whether we should be allowed to do this at all. After all closeEnough expected a List<Point> not a List<ColorPoint>. If we want our lists to be mutable, we have the depth subtyping problem we discussed earlier, and we cannot say that List<ColorPoint> <: List<Point>.
The other problem we have is that of the return value. The definition of closeEnough tells us merely that this is a List<Point>. But in reality it is a List<ColorPoint> as we merely filtered the original list. We have however lost that fact.
So you might say, why can’t we just make closeEnough fully polymorphic:
closeEnough: List<T> -> List<T>
Then we could ensure that whatever we get back has the same type as what went in. The problem is that the computation that closeEnough needs to perform requires something of the provided element, namely that it be a Point so we can measure its distance. This is not possible for a generic type.
What we need is in some sense the best of both worlds: We need a polymorphic type, but we need it to be restricted to only those types that are subtypes of Point. This is bounded polymorphism, where we do have a variable type but in a somewhat restricted way, bounded to be a subtype of a specific type.
With that in mind, the type of closeEnough may look like this:
<T extends Point> closeEnough: List<T> -> List<T>
We can only apply closeEnough to a type that is a subtype of Point. Then closeEnough typechecks because it can count on its elements being subtypes of Point and therefore having the necessary distance properties. And also when we call it with ColorPoint it will then take the place of T and we can guarantee that the return type is List<T>, i.e. List<ColorPoint>.