Why do lower type bounds change the variance position?

258 Views Asked by At

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?

2

There are 2 best solutions below

1
On BEST ANSWER

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.

2
On

Use the ++ method from List to see why the restrictions are needed. Due note, this requires that ++ produce a List[B]:

 def ++[B](that: GenTraversableOnce[B]): List[B] 

with a full signature of

 def ++[B >: A, That](that: GenTraversableOnce[B])(implicit bf: CanBuildFrom[List[A], B, That]): That

So why is it important that [B >: A]. Well, what if we want to combine something such that

 trait Foo
 trait Bar extends Foo

and we have a method that has a signature

 def op(that: List[Foo], other: Foo): List[Foo] = that ++ List(other)

I can pass it a list of type Bar but in order to be able to return it as a List[Foo] I must make the condition that Foo >: Bar so that I can actually do the following

 def see(that: List[Bar]): List[Foo] = op(that, myFoo)

which essentially is doing a List[Bar] ++ List[Foo] to return a type of List[Foo] as expressed though a List[Foo] type. That is why the flip happens.

Now if I tried to enforce that Foo <: Bar I would immediately run into the issue that List[Bar] ++ List[Foo] could not return a list of type Foo (not to mention having it conflict with the definition above.) It would only ever be able to return a List of the least upper bound.