3

Here's some code from this tutorial:

case class ListNode[+T](h: T, t: ListNode[T]) {
  def head: T = h
  def tail: ListNode[T] = t
  def prepend(elem: T): ListNode[T] =
    ListNode(elem, this)
}

The tutorial says:

Unfortunately, this program does not compile, because a covariance annotation is only possible if the type variable is used only in covariant positions. Since type variable T appears as a parameter type of method prepend, this rule is broken.

How is T not in a covariant position in predend, and the other T references (def head: T = h and def tail: ListNode[T] = t), apparently, are covariant?

What I'm asking is why T in prepend is not covariant. This is certainly not covered in Why is Function[-A1,...,+B] not about allowing any supertypes as parameters?, which seems to be what others have directed me to read.

Community
  • 1
  • 1
  • 2
    It is as written. In the other two methods `T` is in a contravariant position, it is _only_ in the return type, while for `prepend` it is also in the parameter (`elem: T`), so it can only be invariant. – Gábor Bakos May 21 '15 at 06:30
  • Consider the following example: `trait Fruit; class Apple extends Fruit; class Pear extends Fruit`. Assume `ListNode` is covariant, so `ListNode[Apple]` is also a `ListNode[Fruit]`: `val node: ListNode[Fruit] = ListNode[Apple](new Apple(), null)`. Now if you `prepend` with another `Fruit`, `node.prepend(new Pear())`, you would clearly not get a (typesafe) `ListNode[Apple]`. – Gábor Bakos May 21 '15 at 06:35

2 Answers2

5

Input parameters of methods are not in covariant positions but contravariant positions. Only the return types of methods are in covariant positions.

If your defination of ListNode class were OK, then I could write code like this:

val list1: ListNode[String] = ListNode("abc", null)
val list2: ListNode[Any] = list1  // list2 and list1 are the same thing
val list3: ListNode[Int] = list2.prepend(1) // def prepend(elem: T): ListNode[T]
val x: Int = list3.tail.head // ERROR, x in fact is "abc"

See, if arguments were covariant positions, then a container could always hold values of another type which has the same ancestor of its real type, and this is definitely WRONG!

So, referring to the source code of scala.collection.immutable.List.::, your class should be defined as this:

case class ListNode[+T](h: T, t: ListNode[T]) {
  def head: T = h
  def tail: ListNode[T] = t
  def prepend[A >: T](elem: A): ListNode[A] = ListNode(elem, this)
}

The argument type A is a new type parameter which is lower bounded to T.

Jacek Laskowski
  • 72,696
  • 27
  • 242
  • 420
nicky_zs
  • 3,633
  • 1
  • 18
  • 26
  • 1
    Regarding line 3 in your first snippet. Since `list2.prepend()` returns a `ListNode[Any]`, and `Any` is a supertype of `Int`, how could this successfully be assigned to `list3: ListNode[Int]`? ListNode is covariant, not contravariant, so val `list3: ListNode[Int] = list2.prepend(1)` should fail, should it not? – Sahand Jun 28 '19 at 10:09
  • You're right. The example is invalid: it attempts to find a contradiction where there is none, because as we know, covariant immutable `List[+A]` from the standard library works just fine (as long as you get the generics right). If you wanted to construct a good counterexample, you'd have to find something that *actually breaks*, not something that *actually works*. – Andrey Tyukin Jun 28 '19 at 11:28
1

An example:

val dogList = ListNode[Dog](...)
val animal = Animal()
val dog = Dog()

dogList.prepend(dog) //works
dogList.prepend(animal) //fails

covariance means that you can use a ListNode[Dog] like a ListNode[Animal]

but:

//if compiler would allow it
val animalList: ListNode[Animal] = dogList
animalList.prepend(dog) //works
animalList.prepend(animal) //would fail, but you expect an animallist to accept any animal
Siphor
  • 2,522
  • 2
  • 13
  • 10