58

I have following class:

case class Box[+A](value: A) {

  def set(a: A): Box[A] = Box(a)

}

And the compiler complain:

Error:(4, 11) covariant type A occurs in contravariant position in type A of value a
  def set(a: A): Box[A] = Box(a)

I was searching a lot about the error, but could not find something useful that help me to understand the error.

Could someone please explain, why the error occurs?

softshipper
  • 32,463
  • 51
  • 192
  • 400

5 Answers5

65

The error message is actually very clear once you understand it. Let's get there together.

You are declaring class Box as covariant in its type parameter A. This means that for any type X extending A (i.e. X <: A), Box[X] can be seen as a Box[A].

To give a clear example, let's consider the Animal type:

sealed abstract class Animal
case class Cat extends Animal
case class Dog extends Animal

If you define Dog <: Animal and Cat <: Animal, then both Box[Dog] and Box[Cat] can be seen as Box[Animal] and you can e.g. create a single collection containing both types and preserve the Box[Animal] type.

Although this property can be very handy in some cases, it also imposes constraints on the operations you can make available on Box. This is why the compiler doesn't allow you to define def set.

If you allow defining

def set(a:A): Unit

then the following code is valid:

val catBox = new Box[Cat]
val animalBox: Box[Animal] = catBox // valid because `Cat <: Animal`
val dog = new Dog
animalBox.set(dog) // This is non-sensical: if `set` mutates `animalBox`, it also mutates `catBox` to no longer contain a `Cat` but a `Dog`

The last line is obviously a problem because catBox will now contain a Dog! The arguments of a method appear in what is called "contravariant position", which is the opposite of covariance. Indeed, if you define Box[-A], then Cat <: Animal implies Box[Cat] >: Box[Animal] (Box[Cat] is a supertype of Box[Animal]). For our example, this is of course non-sensical.

One solution to your problem is to make the Box class immutable (i.e. to not provide any way to change the content of a Box), and instead use the apply method defined in your case class companion to create new boxes. If you need to, you can also define set locally and not expose it anywhere outside Box by declaring it as private[this]. The compiler will allow this because the private[this] guarantees that the last line of our faulty example will not compile since the set method is completely invisible outside of a specific instance of Box.

If for some reason you do not want to create new instances using the apply method, you can also define set as follows.

def set[B >: A](b: B): Box[B] = Box(b)
francoisr
  • 4,407
  • 1
  • 28
  • 48
  • 1
    What does this `Cat <: Animal => Box[Cat] >: Box[Animal]` mean? – softshipper Apr 03 '17 at 09:36
  • If you define `A` as contravariant (`Box[-A]`), then `Box[Cat]` becomes a supertype of `Box[Animal]`, which is the opposite of variance (`Box[+A]`). I clarified this part of my answer. – francoisr Apr 03 '17 at 09:42
  • 2
    How can `Box[Cat]` become a supertype of `Box[Animal]`? The real world a cat can not be a supertype of animal. – softshipper Apr 03 '17 at 09:46
  • What is a contravariant position? – softshipper Apr 03 '17 at 09:54
  • @zero_coding Indeed `Box[Cat] >: Box[Animal]` makes no sense in that particular example. But it will become true if you define `A` as contravariant (define it as -A). All the argument of methods (like `a` in the case of your `set(a: A)` are considered to appear in "contravariant position", meaning their type `A` can only be contravariant (defined as `-A`) or invariant (no '+' or '-'). – francoisr Apr 03 '17 at 10:02
  • It is almost clear. Can you please provide a full example of `Dog` and `Cat` above. – softshipper Apr 03 '17 at 10:08
  • It's a simple hierarchy. It's just an example, and doesn't do much to the explanation. I added that to the answer. – francoisr Apr 03 '17 at 10:13
  • 1
    @Adelin Fair enough. I guess I just needed an intro, and also wanted to point out that the compiler was doing a decent job at informing you. – francoisr Oct 02 '19 at 11:45
  • @francoisr your answer is awesome, sometimes statements like catch my attention ;) – Adelin Oct 02 '19 at 11:47
  • @francoisr This is a very good explanation. I was trying to write an def exists(a: A) method for a covariant type and it refused to compile. Your answer helped me figure it out! Thank you!! – hughj Jan 25 '22 at 16:44
  • @francoisr Why "animalBox.set(dog)" is non-sensical? `val animalBox: Box[Animal]` is declared as box containing Animal, both dog and cat are animals. Setting animmalBox.set(dog) won't change catBox's value, since value is immutable. Set returns just new Box – Teimuraz Mar 05 '23 at 11:56
  • I believe you are confusing two different things: the immutability of a *reference to an object*, and the immutability of the *object itself*. Declaring `a` as a `val` makes it impossible to change `a` so that it refers to another object, but it **does not** make the object itself immutable! Yes, returning a new `Box` in `set` is completely valid, this is what I suggest at the end of my answer. The `set` referred to in the middle the start of my answer is the one proposed by the author which suppposes a mutable `Box`. I've clarified my answer. – francoisr Mar 06 '23 at 19:08
32

Others have already given an answer why the code doesn't compile, but they haven't given a solution on how to make the code compile:

> case class Box[+A](v: A) { def set[B >: A](a: B) = Box(a) }
defined class Box
> trait Animal; case class Cat() extends Animal
defined trait Animal
defined class Cat
> Box(Cat()).set(new Animal{})
res4: Box[Animal] = Box($anon$1@6588b715)
> Box[Cat](Cat()).set[Animal](new Animal{})
res5: Box[Animal] = Box($anon$1@1c30cb85)

The type argument B >: A is a lower bound that tells the compiler to infer a supertype if necessary. As one can see in the example, Animal is inferred when Cat is given.

kiritsuku
  • 52,967
  • 18
  • 114
  • 136
7

Try to understand what it means for your Box[+A] to be covariant in A:

It means that a Box[Dog] should also be a Box[Animal], so any instance of Box[Dog] should have all the methods a Box[Animal] has.

In particular, a Box[Dog] should have a method

set(a: Animal): Box[Animal]

However, it only has a method

set(a: Dog): Box[Dog]

Now, you'd think you can infer the first one from the second, but that's not the case: I do you want to box a Cat using only the second signature? That's not doable, and that's what the compiler tells you: a parameter in a method is a contravariant position (you can only put contravariant (or invariant) type parameters).

Cyrille Corpet
  • 5,265
  • 1
  • 14
  • 31
  • This is crazy, I picked the exact same classes as you did for my example, written at the same time! – francoisr Apr 03 '17 at 09:12
  • 1
    @francoisr Not that crazy, if you look at ~ 90% of examples explaining covariance and contravariance, they use the animals/zoo analogy. – Yuval Itzchakov Apr 03 '17 at 09:17
  • 1
    @YuvalItzchakov yeah, I guess that's the first intuitive explanation that many people get when being introduced to the concept. – francoisr Apr 03 '17 at 09:18
6

in addition to the other answers i'd like to provide another approach:

def set[B >: A](x: B): Box[B] = Box(x)
Andrii Abramov
  • 10,019
  • 9
  • 74
  • 96
Brotbaecker
  • 61
  • 1
  • 1
5

Basically you cant put As in if A is covariant, you can only take it out (ex: returning A). If you wish to put As in, then you would need to make it contravariant.

case class Box[-A](value: A)

Do you want to do both, then just make it invariant

case class Box[A](value: A)

The best is to keep it covariant and get rid of the setter and go for an immutable approach.

Sleiman Jneidi
  • 22,907
  • 14
  • 56
  • 77