Covariant & Contravariant

While writing code in various languages, you must have been noticed by IDEs that the properties, functions, or parameters don’t support covariant or contravariant. I usually feel confused about them. They have a slight similarity. Today, I will use a simple example to explain it to you. Actually, it is not so difficult to understand as you think.

In my opinion, the covariant and the contravariant are the relationship between subtype and supertype. We declare three classes below:

1
2
3
4
5
6
7
8
9
class Animal {}
class Dog {}
class Person: Animal {
func speak() {}
}
class Student: Person {
func doHomework() {}
}
class Teacher: Person {}

The inheritance relationship is Student < Person | Dog < Animal, we say Person is the subclass of Animal, and Student is the subclass of Person. Then we define a function that accepts a function parameter.

1
func execute(_ block: (Person) -> Person) {}

The block takes in a Person value and outputs a Person value. If you want to invoke the execute function, you should pass a (Person) -> Person block to it by default. But we know that we can pass a subtype value into the function if the parameter type is simply a Person type. Is (Student) -> Student the subtype of (Person) -> Person?
I list four situations for that:

(Animal) -> Animal
In the execute function body, maybe passes a Person or Student value to the bloc. Our bloc takes in an Animal parameter. Of course, the subtypes of it are also OK.
Our bock returns an Animal value, and the execute function thinks it will get a Person value via executing the block.
In the execute function body, we use the Person(actually is Animal) result to invoke the speak method, but this usage is unsafe. The block might return a Dog object, and the dog doesn’t have a speak method. Invoking this method will lead to the app crash.

(Student) -> Student
(Student) -> Animal
Just like above, If we pass a Person object to the bloc in the execute function body, the block might use the parameter person to invoke the doHomework method. But a normal Person object doesn’t have the method. Passing these two blocs are also unsafe.

(Animal) -> Student
In the first case, we know the input parameter is Animal is no problem.
If we invoke a Person method to the result of the block in the execute function body, the result type of the block is Student. A student is certainly a person, so passing this bloc is safe.

Conclusion

As you can see, a function parameter, its input params can only take in superclass types(or same type). We term it contravariant. its output params must be subclass types(or same type). We term it covariant.
In Swift, we can not really assign an (Animal) -> Student object to a (Person) -> Person variable.

1
2
3
4
5
6
var v1: (Person) -> Person
func func1(_ v: Animal) -> Student {
return Student()
}
v1 = func1 // Compilation Error
execute(func1) // Compilation Error

The only superclass type we can use is the Any type.

1
2
3
4
5
var v1: (Person) -> Any
func func1(_ v: Any) -> Student {
return Student()
}
v1 = func1 // Compilation Success

Is List<Person> the subclass of List<Animal>?

This question is a bit complicated. Let’s see two examples.

1
2
3
4
func execute(_ animals: [Animal]) {

}
execute([Person()])

We can pass a [Person] value to an [Animal] variable. It looks like [Person] is the subclass of [Animal]. If the animals is immutable, that’s right.
In another case, if the animals is mutable, we can append a dog to it, but its actual type is [Person], insert a dog to a person list? So only an immutable array type can be a subtype of another array type. But the Swift doesn’t support this feature.

Thank you for reading my post. Leave a comment if you have any questions.