Say we have the following types:
A
, B
, A1
and B1
, where A1
is a subtype of A
, and B1
is a subtype of B
. In Java it would look like this:
class A {
...
}
class A1 extends A {
...
}
class B {
...
}
class B1 extends B {
...
}
Now let's build a class of pairs:
class Pair<X, Y> {
X first;
Y second;
}
If we compare
Pair<A, B> ab;
and Pair<A1, B1> a1b1;
we see that it is natural to expect that a1b1 should be assignable to ab, but not vice versa: the components can be cast in one direction only. A1
is a specialization of A
; B1
is specialization of B
, and so the Pair<A1,B1>
is a specialization of Pair<A,B>
. This is what is called covariance: specialization transforms into specialization. And if you are a fan of "Liskov's substitution principle", you can see that a
Pair<A1, B1>
can always be used ("substitute") where a Pair<A, B>
is required.Now the opposite case, contravariance. Suppose we have two functions declared like this:
B f(A a);
B f1(A1 a1);
(I use Java syntax; the two lines above mean that both functions return B).
Which one can replace which? See,
f1
cannot be used instead of f
: what if we pass something that is not A1
? f1
does not know what to do with it. But the opposite is okay: we can always use f
where f1
is required: any A1
can be viewed as an A
.When we deal with object methods, not just plain functions, one hidden parameter is always present: the object itself. So that a method declared as
class A {
...
C f(B b);
...
}
actually is a function defined on pairs (A, B), and takes values in C. This function is contravariant in both parameters. Contravariance in the first (hidden) parameter is an essential part of OOP. Take a look at the following code snippet:
class A1 extends A {
...
C f(B b) {
println(getClass().getName() + " called with " + b.getClass().getName());
}
}
...
A instanceOfA = new A1();
...
instanceOfA.f(b);
...
}
This is Java, and Java uses dynamic dispatch (that is, all methods are virtual), and so the method of
A1
is called in place of method f of A
. Method f
of A
is substituted with a method of A1
; contravariance made it possible.Note that, since a method is always contravariant on its owner object, all getters, for instance, are contravariant: a superclass can call them, and the owner object, which is an instance of a subclass, can substitute the superclass in any context.
(Many thanks to M.Abadi and L.Cardelli's "A Theory of Objects" for explaining the OOP part to me.)
No comments:
Post a Comment