Covariance and Contravariance in Swift

First let’s check if you need this article or not:

This doesn’t work:

var intHandler: (Int) -> Void = { (num) in 
print(num)
}
let anyHandler: (Any) -> Void = intHandler ___ ERROR!

But the opposite works:

let anyHandler: (Any) -> Void = { (any) in
print(any)
}
let intHandler: (Int) -> Void = anyHandler ___ OK.

Lastly, this works:

let intResolverLater: ((Int) -> Void) -> Void = { f in
f(0)
}

var anyResolverLater: ((Any) -> Void) -> Void = intResolver ___ OK.

If you already know what happens, you don’t need this article. But if you’re curious, read on!


You already know the rule that subclass can be used everywhere its parent is used:

class Animal { ... }
class Cat: Animal { ... }
let animal: Animal = Cat()

This behavior is called subtyping. A more-specific type Cat can fit in a bigger container Animal.

In technical term, a subtype of T can be safely used in a context where a term of type T is expected. Cat is a subtype of Animal, and in turns, Animal is a supertype of Cat.

It’s easy when you deal with just one class. But how does it work in these scenarios:

  • Array — Is[Cat] a subtype of [Animal]?
  • Generic — Is PetOwner<Cat> a subtype of PetOwner<Animal> ?
  • Closure — Is (Cat) -> Void a subtype of (Animal) -> Void ?

First one is intuitive; yes. The second is no (later on this).

The last one is no, and surprise, the opposite is true. (Animal) -> Void is a subtype of (Cat) -> Void!

Turns out it’s not black magic. It’s just a (reasonable) choice of a programming language to deal with them that you need to remember. These choices are called covariance and contravariance.

What is Covariance?

Watch closely why [Cat] will be a subtype of [Animal].

First, starting from Cat is a subtype of Animal, written as:

Cat → Animal.

(Let’s refer to this as subtyping direction.)

After thinking through, it makes sense for [Animal] to be able to hold either Cat or Animal. So a grand designer, I simply decide that [Cat] and [Animal] should has the same subtyping direction as Cat → Animal, and we get:

[Cat] → [Animal].

Now [Cat] is a subtype of [Animal]. This decision of using the same subtyping direction as the original types is called covariance.

Another example of covariance is the closure’s return type:

let intBuilder: () -> Int = {
return 5
}
let anyBuilder: () -> Any = intBuilder ___ OK

As you can see, Int is a subtype of Any, and () -> Int is also a subtype of () -> Any. So we can say something like, “closure’s return type in Swift is covariance”.

Guess what is “Contra”variance

It’s a decision to go into the opposite subtyping direction of the original types. But why we ever want to do that?

Well, turns out it makes sense in case of a closure’s parameter. Suppose the code below is valid:

let intHandler: (Int) -> Void = { num in
print(num)
}
let anyHandler: (Any) -> Void = intHandler ___ COMPILE ERROR!

What would happen when we do this?:

anyHandler("Some String")

intHandler doesn’t expect to receive String (or anything else beside Int) as it doesn't know how to handle that. Swift should not let us compile.

Now consider the opposite:

let anyHandler: (Any) -> Void = { (any) in
print(any)
}
let intHandler: (Int) -> Void = anyHandler ___ OK.

This makes sense, why?

We can only throw Int into intHandler, and anyHandler can handle anything, including Int. So, anyHandler should be a valid subtype of intHandler. This means (Any) -> Void can fit in (Int) -> Void!

The way the final subtyping direction of a closure is the opposite of the subtyping direction of the original parameters is called contravariance.

Ok, now let’s look at that problem

Namely, why this code below works:

let intResolverLater: ((Int) -> Void) -> Void = { f in
f(0)
}

var anyResolverLater: ((Any) -> Void) -> Void = intResolver ___ OK.

Let’s build on our intuition step by step.

1. First, the one we’re familiar with (variable name is omitted):

let Any = Int

2. Then, for a function, the parameter on the left (Int) should be a subtype of the parameter on the right (Any):

let (Int) -> Void = (Any) -> Void

Though not obvious, this also states that (Any) -> Void is a subtype of (Int) -> Void.

3. Finally, using the same logic, the parameter on the left ((Any) -> Void) should be a subtype of the parameter on the right ((Int) -> Void), which is already proved by 2., so:

let ((Any) -> Void) -> Void = ((Int) -> Void) -> Void

E.g., it keeps swapping the order on and on!

From a usage viewpoint, try to see that this is valid code:

let intResolverLater: ((Int) -> Void) -> Void = { (f) in
// Use f to handle some Int
f(1000)
}
let anyResolverLater: ((Any) -> Void) -> Void = intResolverLater
// anyResolver must be able to handle Any (can possibly be Int)
let anyResolver: (Any) -> Void = { (any) in
switch any {
case num as Int:
print("Got an int! \(num)")
   ...handle other cases
   }
}
// anyResolver can be used to handle Any (or Int) safely later!
anyResolverLater(anyResolver)

A Visual Aid

You can think of the subtyping behavior of closure/function f: (A) -> B as a water pipe, where its way in and out fit with the rest of the system, and keep the water flowing:

f: (A) — > B

To replace another kind of pipe without leaks (or safely), a new pipe must have a bigger way in (supertype of A) or a smaller way out (subtype of B) than the initial pipe:

Now we get a new pipe f′ that can be used everywhere the initial pipe f is expected, but not vice versa. Thus, closure f′ is a subtype of closure f.

Bonus: Invariance

Well this one’s easy. Int and String is invariance as they are type-incompatible. One can’t be used as another.

Swift’s generic is invariance, which means PetOwner<Cat> is not a subtype of PetOwner<Animal>. They have nothing to do with each other.


Closing

Let’s close this with some gotcha. Why are generics from the standard library such as Array<Animal> covariant, but our own defined generic types (e.g., PetOwner<Animal>) are invariant?

Well, in this case seems like there’s really a magic behind that. Rick said:

“Swift generics are normally invariant, but the Swift standard library collection types — even though those types appear to be regular generic types — use some sort of magic inaccessible to mere mortals that lets them be covariant.”

Reference: https://www.mikeash.com/pyblog/friday-qa-2015-11-20-covariance-and-contravariance.html