Skip to content

Instantly share code, notes, and snippets.

@jimblandy
Last active July 26, 2020 14:24
Show Gist options
  • Save jimblandy/150d110d68aef79739fec83608ebdd93 to your computer and use it in GitHub Desktop.
Save jimblandy/150d110d68aef79739fec83608ebdd93 to your computer and use it in GitHub Desktop.
A simple explanation of covariance and contravariance

If you say "T is a subtype of U", that means that whenever someone wants a U, a T will do: every T works as a U. It follows from this phrasing that U is a subtype of U: certainly if someone wants a U, a U will do.

So if you imagine a type as denoting a set of values, you can write: T ⊆ U.

If every T works as a U, then a function that accepts any U is certainly also a function that accepts any T: fn(U) works as a fn(T). So fn(U) is a subtype of fn(T): fn(U)fn(T).

This is interesting, because the subtypedness gets reversed: T ⊆ U implies fn(U)fn(T). But reread the last paragraph. When you think about it, it does make sense. We call this behavior of fn with respect to its argument type "contravariance", and say "fn(T) is contravariant in T".

Then, consider return values: If every T works as a U, then a function that returns a T is certainly also a function that always returns a U. So fn()->T is a subtype of fn()->U: fn()->Tfn()->U.

In this case, the subtypedness doesn't get reversed: T ⊆ U implies fn()->Tfn()->U. We call this behavior of fn with respect to its return type "covariance", and say that "fn()->T is covariant in T".

@kfogel
Copy link

kfogel commented Nov 2, 2018

Thank you. I had to think about the middle part carefully for a while, but the way you wrote it makes it hard to go off the rails for long.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment