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.
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
.
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