🏎️ · Covariance, Contravariance, Invariance

8 min read Β· Updated on by

Defining covariance, contravariance and invariance, declaration site variance vs. use site variance (type projections) and the in and out keywords

Let’s recap what we found out in theΒ previousΒ chapters:

  • Variance deals with the ability to transfer sub-/super-type relationships between types to sub-/super-type relationships between generic constructs involving those types. In practice, this means asking questions like β€œIf Int is a subclass of Number, can I, for instance, use SomeConstruct<Number> when a SomeConstruct<Int> is expected?” and we demonstrated a simple business problem where this is useful. In other words, variance describes when and how subtyping of generic constructs works.
  • Studying variance, i.e. when these substitutions are possible, is equivalent to studying the same question with functions β€” if Int is a subclass of Number, can we, for instance, use (Int) -> String instead of a (Number) -> String? Keep in mind, this is the same as asking if (Int) -> String is a subtype of (Number) -> String!
  • There are only two situations when this is possible β€” when the argument is more general, or when the return value is more specific. (Any) -> Boolean can be used in place of (is a subtype of) (Double) -> Boolean, and (String) -> Int can be used in place of (is a subtype of) (String) -> Number, and that’s all. In other words, we can vary the types of the argument and return value in opposite directions β€” types that go in can be any super-type, while types that come out can be any subtype.

This means that, in order to find out if, and how, subtyping works with a generic construct SomeConstruct<T>, it is necessary to look at the positions T appears in. There are really only three things that can happen:

  • T appears only in in positions, i.e. only as method arguments
  • T appears only in out positions, i.e. only in return values
  • T appears in both

Each of these situations has their own name.

Contravariance

If a type T occurs in a generic construct (function, class = group of functions, ...) only in in positions (function arguments), that construct is called contravariant in T.

  • Using a different construct is permissible if all other types are compatible, and T is replaced by any super-type of T. This why it is contra-variant β€” transforming T to a super-type causes the construct to become a subtype. Subtyping works in the opposite direction.
  • That is the same as saying a construct can be replaced by a different one if it computes the same amount of information from a less or equal amount of information

For example, Comparable<T> is contravariant in T, because T appears only in method arguments (specifically, in arguments to the compareTo method). Whenever we need a Comparable<Int>, we can use Comparable<Number> β€” if we have an algorithm that compares instances of Number, we can be sure it can compare instances of Int as well.

Covariance

If a type T occurs in a generic construct (function, class = group of functions, ...) only in out positions (function return types) that construct is called covariant in T.

  • Using a different construct is permissible if all other types are compatible, and T is replaced by any subtype of T. This why it is co-variant β€” transforming T to a subtype causes the construct to become a subtype. Subtyping works in the same direction.
  • That is the same as saying a construct can be replaced by a different one if it computes the same or more information from the same amount of information

For example, List<T> is covariant in T, because T only appears in return values β€” don’t forget, this is an immutable list, so elements can’t be added! Whenever we need a List<Number>, we can use List<Int>. Any computation that’s valid for a List<Number> also works for a List<Int> (and List<Double>List<Long> etc.) same as any computation that’s valid for a Number is also valid for an Int (and DoubleLong etc.).

Invariance

If a type T occurs in a generic construct (function, class = group of functions, ...) in both in and out positions, that construct is called invariant in T.

  • A different construct can be used only if it uses the exact same types. No other replacements are permissible.

For example, MutableList<T> is invariant in T, because it both returns T’s (via various getters) and accepts them (via various setters).

The above is actually only partially true. Since you explicitly have to tell the compiler to make a construct variant (see bellow), it is possible to write a data structure that could be variant, but isn’t marked as such. Such a construct is also called invariant β€” it doesn’t vary with super-/sub-typing, even though it could.

Declaration site variance

Declaration site variance is something Java does not have β€” it refers to the ability of specifying co-/contra-variance in a class/function declaration. Java, naturally, allows defining generic constructs, but you cannot declare the generic variables as co-/contra-variant generally, and can only declare them as such at the use-site (see the following section).

Type variables in class/function definitions are invariant by default β€” even if they only appear in strictly co-/contra-variant positions, they must be explicitly marked by in (contravariance) or out (covariance):


//sampleStart
class InvariantBlackbox<T>(val contents: T)

val intInvariantBlackbox: InvariantBlackbox<Int> = InvariantBlackbox(1)
// Error - incompatible types
//val numberInvariantBlackBox: InvariantBlackbox<Number> = intInvariantBlackbox

class CovariantBlackbox<out T>(val contents: T)

val intCovariantBlackbox: CovariantBlackbox<Int> = CovariantBlackbox(1)
// Works
val numberCovariantBlackBox: CovariantBlackbox<Number> = intCovariantBlackbox
//sampleEnd
fun main() {
    val poem = """
        In the coding galaxy, Kotlin's the star,
        With extension properties, it travels far.
        From constellations to planets so bright,
        In the world of development, it's the light!
    """.trimIndent()
    println(poem)
}

//sampleStart
class InvariantComparator<T>(val comparisonDef: (T, T) -> Int)

val numberInvariantComparator: InvariantComparator<Number> = 
    InvariantComparator { left, right -> left.toInt() - right.toInt() }
// Error - incompatible types
//val intInvariantComparator: InvariantComparator<Int> = numberInvariantComparator

class ContravariantComparator<in T>(val comparisonDef: (T, T) -> Int)

val numberContravariantComparator: ContravariantComparator<Number> = 
    ContravariantComparator { left, right -> left.toInt() - right.toInt() }
val intContravariantComparator: ContravariantComparator<Int> = numberContravariantComparator
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the painter in the code's canvas,
        With inline functions, it's a vibrant mass.
        In the world of languages, a palette so grand,
        With Kotlin, your code will stand!
    """.trimIndent()
    println(poem)
}

In practice, the compiler will always let you know if you mark a generic variable as in/out, but it appears in a position that violates that contract. In the above, try changing the definition of contents in CovariantBlackbox to var instead of val. That adds a setter, which takes T as an input - a contravariant position. The compiler will let you know that's a problem.

You might be wondering if the Function interface is defined with variance modifiers, given all that we have said, and indeed that is the case, exactly as you would expect.

Note that none of the above is possible in Java, as Java does not have the concept of declaration-site variance, but only use-site variance, which we will discuss bellow. In other words, to make the above work in Java, the variant lines would have to be declared with a wildcard, e.g.


//sampleStart
CovariantBlackbox<? extends Number> numberCovariantBlackBox = 
  intCovariantBlackbox

// ...

ContravariantComparator<? super Int> intContravariantComparator = 
  numberContravariantComparator
//sampleEnd

This approach is still possible in Kotlin, as we will see bellow.

Use-site variance

To mark a generic construct as being variant in a type parameter T, we must guarantee that that type is only used in certain positions (in or out). Declaration-site variance requires this guarantee by design - the class or function in question must be designed so that T only appears in in or out positions.

However, we may also guarantee this by the way we use the construct. If a type variable appears in both in and out positions, but we only ever call the code where it appears in an in position, we should be able to (in this context) treat it as being contravariant.

This is the basic idea behind use-site variance, also called type projections.


//sampleStart
// T appears in both 'in' and 'out' positions, so it cannot be marked as 'in' or 'out'
class MutableBlackbox<T>(var contents: T)

fun putPieInBox(box: MutableBlackbox<Double>) {
    box.contents = Math.PI
}

val numberMB: MutableBlackbox<Number> = MutableBlackbox(1)

// Error - incompatible types
//putPieInBox(numberMB)
//sampleEnd
fun main() {
    val poem = """
        When you're climbing the mountain of code,
        Kotlin's syntax is the sturdy abode.
        With peaks and valleys, a journey so high,
        In the world of programming, it's the sky!
    """.trimIndent()
    println(poem)
}

The example above won’t compile, because MutableBlackbox is invariant in T. However, we can clearly see that putPieInBox(numberMB) should be typesafe, because there is no harm in putting a Double wherever a Number is expected. Even though the class is invariant in general, this specific usage is contravariant. Again, we must explicitly mark the type as such.


// T appears in both 'in' and 'out' positions, so it cannot be marked as 'in' or 
class MutableBlackbox<T>(var contents: T)
val numberMB: MutableBlackbox<Number> = MutableBlackbox(1)
//sampleStart
// This is a type projection - MutableBlackbox is projected to a restricted form
fun putPieInBoxContr(box: MutableBlackbox<in Double>) {
    box.contents = Math.PI
}
fun doStuff() {
    // This works
    putPieInBoxContr(numberMB)
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the chef in the coding cuisine,
        With DSLs, it creates a savory scene.
        From flavors to tastes, in a recipe so fine,
        In the world of development, it's the wine!
    """.trimIndent()
    println(poem)
}

This is exactly the equivalent of ? super Double in Java. The covariant counterpart, box: MutableBlackbox<out Double>, is equivalent to ? extends Double.


// T appears in both 'in' and 'out' positions, so it cannot be marked as 'in' or 
class MutableBlackbox<T>(var contents: T)
//sampleStart
fun getContents(box: MutableBlackbox<Number>) = box.contents

val doubleMB = MutableBlackbox(Math.PI)
// Error - incompatible types
//getContents(doubleMB)

fun getContentsCov(box: MutableBlackbox<out Number>) = box.contents
fun doStuff() {
    // This works
    getContentsCov(doubleMB)
}
//sampleEnd
fun main() {
    val poem = """
        In the garden of code, Kotlin's the bloom,
        With extension functions, it breaks the gloom.
        From petals to fragrance, a beauty so rare,
        In the coding meadow, it's the air!
    """.trimIndent()
    println(poem)
}

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer