Search code examples
scalageneric-variance

Why do lower type bounds change the variance position?


The Scala Language Specification (Section 4.5 on Variance Annotations, p. 44) says

  • The variance position of a type parameter is the opposite of the variance position of the enclosing type parameter clause.
  • The variance position of the lower bound of a type declaration or type parameter is the opposite of the variance position of the type declaration or parameter.

Using the first point above, it is easy to see (at least formally) that

trait Covariant[+A] {
  def problematic[B <: A](x : B)
}

produces the error message

error: covariant type A occurs in contravariant position in type >: Nothing <: A of type B
       def problematic[B <: A](x : B)

and using the first and the second point it is easy to see that

trait Contravariant[-A] {
  def problematic[B >: A](x : B)
}

produces the error message

error: contravariant type A occurs in covariant position in type >: A <: Any of type B
             def problematic[B >: A](x : B)

As I mention, it's easy to see formally (i.e., following the rules for variance annotations) why these errors occur. However, I can not come up with an example illustrating the need for these restrictions. In contrast, it is very easy to come up with examples that illustrate why method parameters should change variance positions, see e.g. Checking Variance Annotations.

So, my question is the following: Assuming, the two pieces of codes above were allowed, what are the examples of problems that arise? This means, I'm looking for examples similar to this one that illustrate what could go wrong in case the two rules cited above were not used. I'm particularly interested in the example involving lower type bounds.

Note that the answer to Scala type bounds & variance leaves this particular question open, whereas the answer given in The "lower bound" will reverse the variance of a type, but why? seems wrong to me.

Edit: I think the first case can be handled as follows (adapting the example cited above). Assume, the following was allowed

trait Queue[+T] {
  def head : T
  def tail :  Queue[T]
  def enqueue[U <: T](x : U) : Queue[T]
}

Then we could implement

class QueueImplementation[+T] extends Queue[T] {
  /* ... implement Queue here ... */
}

class StrangeIntQueue extends QueueImplementation[Int] {
  override def enqueue[U <: Int](x : U) : Queue[Int] = {
    println(math.sqrt(x))
    super.enqueue(x)
  }
}

and use it as

val x : Queue[Any] = new StrangeIntQueue
x.enqueue("abc")

which is clearly troublesome. However, I can not see how to adapt this in order to show that the combination "contravariant type parameter + lower type bound" is also problematic?


Solution

  • Let's suppose we allow for a class to have a type parameter [-T] and a method on that class to have [U >: T]...

    for come class hierarchy
    Dog <: Mammal <: Animal
    
    class Contra[-X](x: X){
      def problem[Y >: X](y: Y): Y = x // X<:Y so this would be valid
    }
    
    val cMammal:Contra[Mammal] = new Contra(new Mammal)
    
    val a:Animal = cMammal problem new Animal // Animal >: Mammal, this is fine
    val m:Mammal = cMammal problem new Mammal // Mammal >: Mammal, this is fine
    val d:Mammal = cMammal problem new Dog    // (Dog upcasts to Mammal) >: Mammal, this is fine
    
    val cDog:Contra[Dog] = cMammal // Valid assignment
    
    val a:Animal = cDog problem new Animal // Animal >: Mammal, this is fine
    val m:Mammal = cDog problem new Mammal // Mammal >: Mammal, this is fine
    val d:Dog    = cDog problem new Dog    // AAAHHHHHHH!!!!!!
    

    This last line would type check, cDog problem new Dog would actually return a Mammal. This is clearly not a good thing. Thankfully the type system doesn't actually let us do this.

    Q.E.D. contravariant type parameter + lower type bound not a good idea to mix.

    I hope this example helps.