2

I am trying to understand contravariance, how it works. Consider following sentence:

Confusingly, contravariance means that the type F[B] is a subtype of F[A] if A is a subtype of B

The sentence makes me confuse. On the first part F[B] is subtype of F[A] and suddenly why on the second part is A is a subtype of B? It contradicts itself?

Covariance is more clear:

Covariance means that the type F[B] is a subtype of the type F[A] if B is a subtype of A .

On the first part is F[B] is subtype and also on the second B is subtype.

softshipper
  • 32,463
  • 51
  • 192
  • 400
  • That's it. Covariance is the "natural" for our understanding. Contravariance means that F[B] is subtype of F[A] while A is subtype of B. A and F[A] are different things, so it's not contradictory, it's just confusing :) You can check this answer, I think it's pretty helpful https://stackoverflow.com/questions/27414991/contravariance-vs-covariance-in-scala – SCouto Jan 31 '18 at 14:12
  • `JsonWriter[Shape] is a subtype of JsonWriter[Circle] because Circle is a subtype of Shape` for me it does not make sense at all. – softshipper Jan 31 '18 at 14:34
  • Contravariance is the 'other direction' to covariance, another way to state the above would be `contravariance means F[B] is a subtype of F[A] if A is a supertype of B`. – Lee Jan 31 '18 at 14:52
  • Is contravariance really that surprising? There are many things that contravary, that you don't think twice about. For example, the value and the absolute magnitude of a negative number are contravariant: as the value gets larger, the magnitude gets smaller and vice versa. – Jörg W Mittag Feb 03 '18 at 11:03

2 Answers2

3

Using the Json example in the comments of this question:

trait Shape {
  val area: Double
}

case class Circle(radius: Double) extends Shape {
  override val area = math.Pi * radius * radius
}

def writeJson(circles: List[Circle], jsonWritingFunction: Circle => String): String =
  circles.map(jsonWritingFunction).mkString("\n")

def circleWriter(circle: Circle): String =
  s"""{ "type" : "circle writer", radius : "${circle.radius}", "area" : "${circle.area}" }"""

def shapeWriter(shape: Shape): String =
  s"""{ "type" : "shape writer", "area" : "${shape.area}" }"""

Then both of these are acceptable:

writeJson(List(Circle(1), Circle(2)), circleWriter)
writeJson(List(Circle(1), Circle(2)), shapeWriter)

And result in

// first writeJson
{ "type" : "circle writer", "radius" : "1.0", "area" : "3.141592653589793" }
{ "type" : "circle writer", "radius" : "2.0", "area" : "12.566370614359172" }
// first writeJson
{ "type" : "shape writer", "area" : "3.141592653589793" }
{ "type" : "shape writer", "area" : "12.566370614359172" }

Even though jsonWritingFunction expects a Circle => String we can pass it a Shape => String due to Function1's declaration: trait Function1[-T1, +R]. The first type (T1) is contravariant.

Hence Shape => String is a subtype of Circle => String because Circle is a subtype of Shape.

Tom
  • 2,214
  • 2
  • 16
  • 28
  • Your last sentence sounds contradicted to me, it sounds for me, first is `shape` a subtype of `circle` and on the second time `circle` is a subtype of `shape`, so that makes me confused. – softshipper Feb 01 '18 at 07:15
  • "first is `shape` a subtype of `circle`" - this isn't true because you _cannot_ take it out of context of the complex type. You _have to_ say `Shape => String` is a subtype of `Circle => String`, you can't just get rid of the `=> ...` stuff. (It is confusing, but it's no coincidence that **contra**dictory and **contra**variance share the same prefix!) – Tom Feb 01 '18 at 09:31
1

I have an intuition that I find helpful to understand covariance and contravariance. Note that this is not a strict definition though. The intuition is following:

  1. if some class only outputs values of the type A, which is the same as saying that the users of the class can only read values of the type A from the class, it is covariant in the type A
  2. if some class only takes values of the type A as inputs, which is the same as saying that the users of the class can only write values of the type A to the class, it is contravariant in the type A

For a simple example consider two interfaces Producer[A] and Consumer[A]:

trait Producer[A] {
   def produce():A
}

trait Consumer[A] {
   def consume(value:A):Unit
}

One just outputs values of type A (so you "read" A from Producer[A]) while the other accepts them as a parameter (so you "write" A to the Producer[A]).

Now consider method connect:

def connect[A](producer:Producer[A], consumer:Consumer[A]): Unit = {
  val value = producer.produce()
  consumer.consume(value)
}

If you think for a moment this connect is not written in the most generic way. Consider types hierarchy Parent <: Main <: Child.

  1. For a fixed Consumer[Main] it can process both Main and Child because any Child is actually Main. So Consumer[Main] can be safely connected to both Producer[Main] and Producer[Child].
  2. Now consider fixed Producer[Main]. It produces Main. Which Consumers can handle that? Obviously Consumer[Main] and Consumer[Base] because every Main is Base. However Consumer[Child] can't safely handle that because not every Main is Child

So one solution to creating the most generic connect would be to write it like this:

def connect[A <: B, B](producer:Producer[A], consumer:Consumer[B]): Unit = {
  val value = producer.produce()
  consumer.consume(value)
}

In other words, we explicitly say that there are two different generic types A and B and one is a parent of another.

Another solution would be to modify Producer and Consumer types in such a way that an argument of type Producer[A] would accept any Producer that is safe in this context and similarly that an argument of type Consumer[A] would accept any Consumer that is safe in this context. And as you may have already noticed the rules for Producer are "covariant" but the rules for "Consumer" are "contravariant" (because remember you want Consumer[Base] to be a safe subtype of Consmer[Main]). So the alternative solution is to write:

trait Producer[+A] {
   def produce():A
}

trait Consumer[-A] {
   def consume(value:A):Unit
}

def connect[A](producer:Producer[A], consumer:Consumer[A]): Unit = {
  val value = producer.produce()
  consumer.consume(value)
}

This solution is better because it covers all cases by a single change. Obviously in any context that Consumer[Main] is safe to be used Consumer[Base] is also safe.

SergGr
  • 23,570
  • 2
  • 30
  • 51