-1

I've just begun learning Scala, I'm going thru Odersky's course and I'm stuck on understanding variances. I am also referring to this blog - https://medium.com/@wiemzin/variances-in-scala-9c7d17af9dc4

I do not understand the examples where the objects extend type A but a compile error is still thrown:

sealed trait A
case object B extends A
case object C extends A
case object D extends A

and then later:

val list: List[A] = Cons(B, Cons(C, Cons(D, Nil()))) //compile error: class List is invariant in type A.

I fail to understand why the compile fails. Objects B, C, D do extend A, are sub types of A so why isn't this operation allowed?


The terms "covariant" and "contravariant" somehow do not seem intuitive to me and I fail to understand what exactly is being conveyed in the blog post.

Is it a way to enforce strict compile time type checking? I come from a Java background but somehow I find this all very confusing.

Saturnian
  • 1,686
  • 6
  • 39
  • 65

1 Answers1

2

The purpose of the variance annotations is to prevent, at compile time, that you don't end up with a value you don't expect.

Consider

sealed trait Animal
class Dog() extends Animal

There are three meaningful types of variance.

  • Covariance: a CovariantOf[Dog] is a subtype of CovariantOf[Animal]
  • Contravariance: a ContravariantOf[Animal] is a subtype of ContravariantOf[Dog]
  • Invariance: there's no relationship between InvariantOf[Animal] and InvariantOf[Dog] (i.e. neither covariant nor contravariant)

Let's first consider something that we can get things out of:

trait Producer[A] {
  def getOne: A
}

Let's imagine that Producer is contravariant: a Producer[Animal] is a Producer[Dog]. Then we could have:

class Wildebeest() extends Animal
val animalProducer = new Producer[Animal] { def getOne: Animal = new Wildebeest() }
val dogProducer: Producer[Dog] = animalProducer
val dog: Dog = dogProducer.getOne

animalProducer only promises to produce Animals and in fact will only produce Wildebeests, but the last line is expecting a Dog. The only hinky thing is treating the Producer[Animal] as a Producer[Dog], since a general Producer[Animal] isn't promising anything about only producing Dogs. Since contravariance means that we can do this, declaring Producer to be contravariant can't be legal.

On the flip-side, if Producer were covariant, we could

val dogProducer = new Producer[Dog] { def getOne: Dog = new Dog() }
val animalProducer: Producer[Animal] = dogProducer
val animal: Animal = animalProducer.getOne

And that's clearly OK, since a Dog is an Animal, so it would be OK to declare

trait Producer[+A] {
  def getOne: A
}

Meanwhile, let's model something that we can only produce Animals to: Animals go in but they don't come out:

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

Should Consumer be covariant? No, because imagine that we have a dog-petter:

val dogPetter = new Consumer[Dog] {
  def consume(dog: Dog): Unit = println("petting a dog")
}

dogPetter only knows how to pet dogs. It would be silly to have it pet a slug. But if we made Consumer covariant, we could make it pet slugs:

 class Slug() extends Animal
 val animalPetter: Consumer[Animal] = dogPetter  // no cast needed, since covariance would mean a Consumer[Dog] is a Consumer[Animal]
 val slug = new Slug()
 animalPetter.consume(slug)  // Uh-oh, the dogPetter is petting a slug!

dogPetter's definition is perfectly fine. Treating a Slug as an animal is perfectly fine: a Consumer[Animal] should definitely be able to consume a Slug. So the problem must be with treating a dogPetter as an Animal-petter, which means the problem is with covariance.

On the flip-side, imagine that we had an object that could praise an Animal, any Animal:

val animalPraiser = new Consumer[Animal] {
  def consume(a: Animal): Unit = println("I praise this ${a.getClass} (like I should)")
 }

It can clearly praise a Dog or a Slug or any other Animal, so this should be valid:

val dogPraiser: Consumer[Dog] = animalPraiser
dogPraiser.consume(new Dog)

Thus, Consumer can be contravariant:

class Consumer[-A] { ... }

The intuition (which is absolutely correct) is that a type which is generic in A can only be covariant in A if it does not consume As; likewise, it can only be contravariant in A if it does not produce As.

What if we have something that we can produce to and consume from? Well, then it must be invariant because it can't be covariant and it can't be invariant. Invariance is a conservative default, since assuming covariance or contravariance could lead to some unsound places.

Java likewise has covariance and contravariance annotations:

  • Foo<? extends Bar> for covariance
  • Foo<? super Bar> for contravariance

The key difference with Scala is that in Java you would have the annotation at the use-site while in Scala it's at the definition site. The Scala approach, if nothing else, reduces the repetitive variance annotations.

Levi Ramsey
  • 18,884
  • 1
  • 16
  • 30
  • 1
    Just a couple of comments. 1) The fourth variance https://typelevel.org/blog/2016/09/19/variance-phantom.html 2) Besides variances (co/contra/in) of type constructors with respect to each of their type parameters there are also variant type positions (co/contra/in). – Dmytro Mitin Sep 21 '20 at 01:20
  • I think my problem is with understanding what the words truly mean/imply. While I understand covariance, I fail to understand contravarience. Going by your Java example, covariance implies a subtype relationship whereas contravarience implies some sort of supertype relationship? Am I getting this right? – Saturnian Sep 21 '20 at 03:03
  • They both imply subtype & supertype relationships. – Levi Ramsey Sep 21 '20 at 12:47
  • Functions are contravariant in their arguments: imagine that you have a Java method `public void f(Bar b)`. You could change that to `public void f(Object b)` (`Object` being a supertype of `Bar`) and no place where you're passing a `Bar` would change. Since you can use (to switch to a hybrid notation, since I haven't done Java in a long time) a `Function[Object, void]` wherever you use a `Function[Bar, void]`, the former is a subtype of the latter (even though `Object` is a supertype of `Bar`). That relationship is all that contravariance is. – Levi Ramsey Sep 21 '20 at 13:06
  • Conversely, functions are covariant in their results: `public Object g(int i)` can be changed to `public Bar g(int i)` and no place where you were calling `g` before would need to be changed, so (see previous comment about hybrid notation) `Function[int, Bar]` is a subtype of `Function[int, Object]`. – Levi Ramsey Sep 21 '20 at 13:10