Variance from Subtyping’s view
This concept has somehow got me intrigued on a few occasions and I am sure majority of the readers find this interesting too.
Let’s start with what is a “variance”? In simple terms, it is about allowing a type to be interpreted as another type, provided there is some (subtyping) relation between the two.
If type T
is a subtype of type U
, then T
can be provided wherever U
is expected.
T some_t = create_T();
U some_u = t;
which is:
Cat some_cat = create_cat("fluffy");
Mammal some_mammal = cat; Animal some_animal = mammal;
// or
Animal some_animal = cat;
As you can see above, from the subtyping relation (<:
), we have:
T <: U
Cat <: Mammal <: Animal
Cat
is a subtype of Mammal
and then further also of Animal
, and Mammal
is a subtype of Animal
. The relation is transitive.
The other way around it is called supertyping (>:
). Animal
is a supertype of Mammal
& Cat
and Mammal
is a supertype of Cat
.
Animal >: Mammal >: Cat
Covariance
So if we have a list of Cat
, we can assign it to a type that is a list of Animal
, or if we have a function that takes an Animal
we can provide it a Cat
.
Func (Animal a) {
// blah blah
}List[Cat] list_of_cats = create_list_of_cats();Func(list_of_cats[0]);
List[Animal] = list_of_cats;
This is called Covariance. We can say that List[A]
is covariant over A
. i.e List[T] <: List[U]
provided T <: U
. You can give T
to Func(U)
, provided T <: U
.
So we can say that covariance increases the “scope” of a type’s usage. Animal
can be used “more widely” than Cat
.
What would happen, if we could give Animal
where Cat
is expected. Let’s look at an example:
Dog dog = create_dog("max");
Animal animal = dog; //covarianceCat cat = animal; //assuming this is allowed
cat.meow(); //**Error**
As we can see, animal
above is actually a Dog
and obviously this is incorrect.
Dogs don’t meow!
Contravariance
This is a bit confusing to understand at start, but simple enough once you get the reasoning behind it.
Let’s consider we have a function Func(A)
. So as a type, Func(A)
is contravariant over it’s input argument(s).
i.e
If T <: U
, then Func(U) <: Func(T)
. Yes, you read it right. Not a typo. It basically says you can give Func(U)
where Func(T)
is expected.
Func(Animal) f_animal_input = provide_some_func_that_takes_animal();
Func(Cat) f_cat_input = f_animal_input;
Here, we are basically narrowing down the “scope” of the type’s usage. f_cat_input()
is “less widely” usable than f_animal_input()
. f_animal_input()
obviously can take in any Animal
. It’s just that we are now restricting it to only accept a Cat
.
Now, let’s see what would happen if we were allowed to give Func(Cat)
, where Func(Animal)
is expected.
provide_some_func_that_takes_cat() {
feed_the_cat (Cat c) {
feed_whiskas (c)
}
return feed_the_cat;
}Func(Cat) f_cat_input = provide_some_func_that_takes_cat();
Func(Animal) f_animal_input = f_cat_input;//assuming this is allowedDog dog = create_dog("max");
f_animal_input (dog); //**Error**
We are passing Dog
to f_animal_input()
which actually is fine, as per the covariance rules, since it is of type Func(Animal)
. But f_animal_input()
is actually a Func(Cat)
. So essentially you have a function that’s suppose to handle aCat
, now handle a Dog
.
You don’t feed cat food to a dog!
Covariance, once more
So now that we understand that a function is contravariant over it’s input arguments. The next obvious question would be “Does contravariance also hold true for return arguments as well?”
Take a moment to think.
If your answer is no, then you are bang on!
Function is covariant over it’s return arguments. i.e you are allowed to give CreateCat() -> Cat
where CreateAnimal() -> Animal
is expected.
This should be easy to reason about. Just the way we think about a List[A]
being covariant over A
, Func() -> A
is covariant over A
too.
Invariance
This is relatively simple to understand. If there is no subtyping relationship between the types, then you cannot use one in place of another. You cannot give a Cat
, where a Dog
is expected, just like you cannot give aSpacecraft
where an Orange
is expected.
You see the point? If I am in Japan, then I will have to buy stuff there in Yen
. I cannot carry transactions in Rupee
, whereYen
is expected. So that’s an invariant, which needs to be upheld.
Mutability & Variance
Let’s take a look at the covariance example we covered in the beginning. What if the List[A]
was mutable?
1 MutableList[Cat] list_of_cats = create_mutable_list_of_cats();
2 MutableList[Animals] list_of_animals = list_of_cats;
3
4 Dog dog = create_dog("max");
5 list_of_animals.add(dog);
6
7 feed_the_cats(list_of_cats); // feed_the_cats(List[Cat])
If you read the carefully, there are a few problems in the above code. Let’s break it down:
- We create a mutable list of
Cat
and assign to mutable list ofAnimal
, as per the covaraince rules. - We then add a
Dog
to thelist_of_animals
, sinceDog
is anAnimal
. See the problem yet? - Further we pass
list_of_cats
to the functionfeed_the_cats()
. Now this where we hit a problem. Thelist_of_cats
being mutable was added aDog
aslist_of_animals
on line no. 5. Butfeed_the_cats()
, expects aList[Cat]
, which is not truly aList[Cat]
anymore, as we have added aDog
to it.
But the code seems fine, as it “seems” to obey the variance rules. Well, not really. The line no. 2 above is actually problematic. SomeType[A]
can be covariant over A
, provided SomeType
is immutable, otherwise it is invariant. So in the above case MutableList[A]
is invariant over A
and the statement MutableList[Animals] list_of_animals = list_of_cats;
is actually incorrect. We cannot treat MutableList[Cat]
as a MutableList[Animal]
.
Closing Points
Well, I hope this helped in understanding a point or two about variance and subtyping. You won’t have to dealt with these concpets in your everyday programming, but nontheless are one of the very important aspects of understanding types in general.
Special mention: Although this video is based on Rust, I found it very helfpul in understanding the concept better.