DEV Community

Venkatesh-Prasad Ranganath
Venkatesh-Prasad Ranganath

Posted on

2 1

Covariance and Contravariance

In the below Kotlin code snippet, Producer class is covariant in type parameter T and Consumer class is contravariant in type parameter T. In other words, T is a covariant type parameter of Producer and T is a covariant type parameter of Consumer. So, what do these statements mean?

To understand the meaning of these statements, let’s consider the allowed behaviors. Copy-n-paste the above code snippet at https://try.kotlinlang.org and run it. You will observe two errors — at lines 13 and 15.

class Producer<out T: Any>(val e:T) {
fun read(): T = e
}
class Consumer<in T: Any>() {
private lateinit var e: T
fun write(v: T): Unit { e = v }
}
fun main() {
var p1: Producer<Number> = Producer<Double>(0.4)
p1.read()
var p2: Producer<Double> = Producer<Number>(3) // Disallowed
p2.read()
var c1: Consumer<Number> = Consumer<Double>() // Disallowed
c1.write(3)
var c2: Consumer<Double> = Consumer<Number>()
c2.write(0.4)
}
view raw variance.kt hosted with ❤ by GitHub

Covariance

The assignment on line 11 is allowed as Producer<Double> is treated as a sub-type of Producer<Number>. This is due to two reasons.

  • Values of type T can only be retrieved/read from Producer<T> instances because T is used to specify only the values provided by Producer; see the class definition of Producer.
  • Since Producer<Double> instance is assigned to p1, all values retrieved/read via p1 will be of type Double, a sub-type of the type argument Number of the declared type Produce<Number> of p1. So, the behavior exhibited by the instance assigned to p1 conforms with (is a subset of) the behavior guaranteed by the declared type of p1.

Due to the above sub-typing relation, the assignment on line 13 is not allowed. If it was allowed, p2.read() would return a value of type Number when it should return only values of type Double based on the declared type Producer<Double> of p2.

Contravariance

A similar reasoning explains contravariance.

The assignment on line 17 is allowed as Consumer<Number> is treated as a sub-type of Consumer<Double>. This is due to two reasons.

  • Values of type T can only be injected/written into Consumer<T> instances because T is used to specify only the values provided to Consumer; see the class definition of Consumer.
  • Since the type argument Double of the declared type of c2 is a sub-type of Number, all values provided to Consumer<Number> instance assigned to c2 will be a sub-type of type Number. The behavior allowed by the declared type of c2 conforms with (is a subset of) the behavior supported by the instance assigned to c2.

Due to the above sub-typing relation, the assignment on line 15 is not allowed. If it was allowed, c1.write(3) would store a value of type Number in the property (field) c1.e when it should store only values of type Double in c1.e based on the Consumer<Double> instance assigned to c1.

Summary

In short, if type X is a sub-type of type Y and type S<X> can be treated as sub-type of type S<Y>, then S is covariant in T and T is a covariant type parameter of the generic type S<T>.

Similarly, if type X is a sub-type of type Y and S<Y> can be treated as sub-type of type S<X>, then S is contravariant in T and T is a contravariant type parameter of the generic type S<T>.

In both definitions, the key is if S<X> will be treated as sub-type of S<Y> or vice versa, and this is dependent on how the type parameter T of S<T> is used in S; specifically, in the context of input and output of the operations/methods of S.

Further Reading

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more

Top comments (0)