Search code examples
scalacovariancegeneric-variance

Scala covariance and lower bound i dont get it


I am currently learning scala and i am confused about the variance annotations especially the covariance and contravariance.

So i did some research an came upon following example

 class Box[+T] {

    def put[U >: T](x: U): List[U] = {
      List(x)
    }

  }

  class Animal {

  }

  class Cat extends Animal {

  }

  class Dog extends Animal {

  }

  var sb: Box[Animal] = new Box[Cat];

So this says class Box is covariant in T whiche means Box[Cat] is a subclass of Box[Animal] since Cat is a subclass of Animal. Sofar i understand this. But when it comes to method parameters my understanding ends. The spec says methods parameters can not be covariant so we have to use this lower bound annotation.

Lets look at the method definiton

 def put[U >: T](x: U): List[U] = {
   List(x)
 }

So [U >: T] says that U must be a superclass of T

Trying following code

  var sb: Box[Animal] = new Box[Cat];
  sb.put(new Cat);

Works as expected but this drives me nuts

  var sb: Box[Animal] = new Box[Cat];
  sb.put(1);

It logically makes no sense to me to put an INT into a Box of Animals alltough its correct since INT will be resolved to Any which is a superclass of Animal.

So my question is

How do i have to adapt the code that the put method accepts only subtypes of animal? I can not use the upper bound annotation

class Box[+T] {

    def put[U <: T](x: U): List[U] = {
      List(x)
    }

  }

since i get this well known error

covariant type T occurs in contravariant position in type


Solution

  • You can add both a lower and an upper bound:

    class Box[+T] { def put[U >: T <: Animal](x: U): List[U] = List(x) }
    

    But this is not what you want, since you wire the definition of Box to Animal and logically there is no reason to add such an upper bound.

    You say:

    It logically makes no sense to me to put an INT into a Box of Animals alltough its correct since INT will be resolved to Any which is a superclass of Animal.

    You don't put an Int into a Box[Animal], the existing box is immutable (and it is not possible to make it mutable, since the definition of covariance doesn't allow it). Instead you get a box (or in case of your put method) of a new type. If your goal is to get only a List[Anmial], then you just have to specify that:

    scala> class Box[+T] { def put[U >: T](x: U): List[U] = List(x) }
    defined class Box
    
    scala> var b: Box[Animal] = new Box[Cat]
    b: Box[Animal] = Box@23041911
    
    scala> val xs: List[Animal] = b put new Dog
    xs: List[Animal] = List(Dog@f8d6ec4)
    
    scala> val xs: List[Animal] = b put 1
    <console>:14: error: type mismatch;
     found   : Int(1)
     required: Animal
           val xs: List[Animal] = b put 1
                                        ^
    
    scala> val xs = b put 1 // this will result in a List[Any]
    xs: List[Any] = List(1)
    

    There is no need to complicate the definition of the put method.

    For a more in depth explanation about why co- and contravariance is needed see this question.