How to Determine Subtyping Relations Based on Variance Information
Introduction
Subtyping is directly related to the Liskov Substitution Principle, which states that objects of a superclass shall be replaceable with objects of a subclass without affecting the correctness of that program. For example, if class B
is a subclass of class A
, then we should be able to replace A
with B
without disrupting the behavior of our program.
While covariant and contravariant are rules we use to determine the subtype relationship between two generic types. In our Exercise 2, we say this:
- Lists are covariant in their only type parameter.
- Functions are contravariant in the argument, and covariant in the result.
You see? When we mention covariance or contravariance, we must point out which type parameter of which generic type we are talking about. Let’s focus on covariance first.
Covariance and List Types
When we say that a type parameter of a generic type is covariant, it means that if there are two types S
and T
, where S
is a subtype of T
, then filling in this type parameter with S
and T
respectively maintains a subtype relationship of same direction.
First, let’s take a look at the first two questions. Banana
is a subclass of Fruit
, so Banana
is a subtype of Fruit
(written as Banana <: Fruit
). We also know that the only type parameter of List
is covariant. So when we fill Banana
and Fruit
into the type parameter of List
, the resulting types List[Banana]
and List[Fruit]
still maintain the same direction of subtype relationship, that is, List[Banana] <: List[Fruit]
.
Using the same method, we can also determine the relationship between List[A]
and List[B]
.
If you go to look at the documentation of Scala (c.f. List), or the code of the standard library, you will find that there is a +
symbol in front of the type parameter of List.
sealed abstract class List[+A] extends // ...
The +
indicates that type parameter A is covariant.
Covariance and Tuple Types
Similar to the List type, all type parameters of the Tuple type (regardless of how many type parameters it has) are covariant.
So I think you can also judge the relationship between the following types very quickly.
(Banana, Fruit)
and(Fruit, Fruit)
(Banana, Juice)
and(Fruit, Liquid)
(Banana, Fruit)
and(Fruit, Banana)
The key to solving the problem is to ensure that each type parameter of the two tuple types maintains the same direction of the subtyping relation. Don't forget that the subtyping relationship is reflexive, meaning that for any type T
, we have T <: T
.
It's not difficult to see that the relationships in the first two examples above are both <:
, while the relationship in the last example is incomparable.
Function Types and Contravariance
Next, let's demystify the subtyping relation of function types. Many people find this challenging, but it's not really. First, the function type T => R
can be written as Function1[-T, +R]
. Here, the -
symbol indicates that the type parameter T
is contravariant.
Do you still remember how to determine the subtyping relation for Tuple types? If there are two types, (A1, B1)
and (A2, B2)
, the subtyping direction of A1
and A2
needs to be the same as the subtyping direction of B1
and B2
.
Now, with one type parameter of Function1
being contravariant and the other covariant, this means their subtyping relations must be opposite to derive the subtyping relation of Function1
.
That is, given A1 => B1
and A2 => B2
, there are two possible cases.
- (Case 1) If
A1 <: A1
andB1 >: B2
, then(A1 => B1) >: (A2 => B2)
. - (Case 2) If
A1 >: A1
andB1 <: B2
, then(A1 => B1) <: (A2 => B2)
.
For other cases, A1 => B1
is not comparable with A2 => B2
.
Apply What We’ve Learned
Let’s apply this to some questions!
Banana => Juice
andFruit => Juice
: SinceBanana <: Fruit
andJuice >: Juice
, we have(Banana => Juice) >: (Fruit => Juice)
(by applying Case 1).Banana => Juice
andBanana => Liquid
: SinceJuice <: Liquid
andBanana >: Banana
, we have(Banana => Juice) <: (Banana => Liquid)
. (by applying Case 2).
The above two are both a piece of cake. Let's take a look at some challenging problems that leave some people confused. But they're actually not difficult; what we need to do is a step by step reasoning.
-
(Fruit => Juice) => Liquid
and(Banana => Liquid) => Liquid
Let’s do this in three steps.
- We need to judge the relation between
Fruit => Juice
andBanana => Liquid
. SinceFruit >: Banana
andJuice <: Liquid
, we have(Fruit => Juice) <: (Banana => Liquid)
(by applying Case 2). - We need to judge the relation between
Liquid
andLiquid
, this is immediate. That isLiquid >: Liquid
. - By the conclusion of both 1 and 2, we have
((Fruit => Juice) => Liquid) >: ((Banana => Liquid) => Liquid)
(by applying Case 2).
Therefore, the answer is
>:
. - We need to judge the relation between
-
Fruit => (Juice => Liquid)
andBanana => (Liquid => Liquid)
.Similarly, let’s do this in three steps.
- The subtyping relation between types of function parameters are immediate, that is,
Fruit >: Banana
. - Let’s see the subtyping relation between
Juice => Liquid
andLiquid => Liquid
. SinceJuice <: Liquid
andLiquid >: Liquid
, we have(Juice => Liquid) >: (Liquid => Liquid)
. - The conclusion of both 1 and 2 matches neither Case 1 nor Case 2.
Therefore, they are not comparable.
- The subtyping relation between types of function parameters are immediate, that is,
I hope this tutorial will be of help to you!