1

The top answer of this question explains why method arguments are contravariant. The author says that if this would compile:

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)
}

Then this would be okay:

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]

Note that I have removed the last row of the original snippet.

My thought process is the following:

  1. On line 1, a ListNode[String] called list1 is assigned a new ListNode(someString, null). Nothing strange here. list1 is not a ListNode[String].
  2. On line 2, a ListNode[Any] gets assigned list1. This is fine because ListNode is covariant and Any is a supertype of String.
  3. On line 3, the prepend method of list2 is called. Since list2 is a ListNode[Any], list2.prepend() should return a ListNode[Any]. But the result of the method call is assigned to a ListNode[Int]. This can not possibly compile because Int is a subtype of Any and ListNode is NOT contravariant!

Have I misunderstood something? How can the author claim this would ever compile?

Sahand
  • 7,980
  • 23
  • 69
  • 137
  • 2
    I think you might be interested in [this question](https://stackoverflow.com/questions/37334674/why-method-defined-like-consb-av-b-accepts-argument-of-type-which-is-n) and its answer. – TeWu Jun 28 '19 at 11:11

2 Answers2

-1

Let consider what will happen if we define as below:

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)
}

If you look into prepend method, it take T (which is type parameter of ListNode) as parameter and return ListNode[T], simple as it is. Now, let elaborate the usage:

val list1: ListNode[String] = ListNode("abc", null)

In above case, String is supertype of null, and since ListNode is defined covarient, it is correct.

val list2: ListNode[Any] = list1

In above second case, ListNode[String] is assigned to ListNode[Any] since Any is supertype of String which is also correct.

val list3: ListNode[Int] = list2.prepend(1)

Finally in above third case, what we are doing is prepending 1 which is Int. If you look into method prepend(elem: T): ListNode[T], we are passing Int as type of elem value and its returning ListNode of type T; in this case, ListNode of type Int. Hence, value returned from invoking list2.prepend(1) is of type ListNode[Int]. So, above execution for list3 is also possible (and correct theoritically).

However, in scala, you cannot define def prepend(elem: T): ListNode[T] in case of covariant type (you will get compilation error) and hence you will be never able to do val list3: ListNode[Int] = list2.prepend(1). But instead you can use lower-bounds as below:

case class ListNode[+T](h: T, t: ListNode[T]) {
  def head: T = h
  def tail: ListNode[T] = t
  def prepend[U<:T](elem: U): ListNode[U] =
    ListNode[U](elem, this)
}
Ra Ka
  • 2,995
  • 3
  • 23
  • 31
-3

Consider the prepend method in isolation:

def prepend(elem: T): ListNode[T]

This signature means that if you give it a T you get ListNode[T]. If you give it Int you get ListNode[Int] so the assignment that you are questioning is perfectly valid.

Also remember that the author is saying that this version of ListNode doesn't compile, so it is a hypothetical situation.

Tim
  • 26,753
  • 2
  • 16
  • 29
  • I think this is a good answer. Why is it downvoted? Is something incorrect? – Sahand Jun 28 '19 at 11:05
  • 6
    @Sahand The signature is not `def prepend[T](elem: T): ListNode[T]`, it's `def prepend(elem: T): ListNode[T]`. The type parameter `T` is bound in the enclosing class, it is not bound for each new `elem`. If `T` is bound to `Any`, and you give it `elem: Int <: Any`, you get a `ListNode[Any]` back, not `ListNode[Int]`, so the first paragraph seems invalid. The last sentence is kind-of true, though: "from falsehood, anything follows". – Andrey Tyukin Jun 28 '19 at 11:10
  • So in that case, I was right? `val list3: ListNode[Int] = list2.prepend(1)` will not compile? – Sahand Jun 28 '19 at 11:25
  • 2
    @Sahand Yes, you are right, it wouldn't compile anyway. But that's not the point. The point is that if e.g. a mutable `HashSet[+A]` with method `+=(elem: A)` were covariant, then it would lead to unsoundness: `import scala.collection.mutable.HashSet ; val h: HashSet[String] = HashSet.empty ; val g: HashSet[Any] = h.asInstanceOf[HashSet[Any]] /* pretend it's covariant */ ; g += 42 ; val s: String = h.head // Class cast exception`. – Andrey Tyukin Jun 28 '19 at 11:40
  • Thanks for the explanation. Is it true that in your example `h` and `g` are the same object in memory? – Sahand Jun 28 '19 at 11:53
  • 2
    @Sahand Yes, the assignment says essentially: `g = h`, all the type annotations and `asInstanceOf`s are for the type checker only. – Andrey Tyukin Jun 28 '19 at 12:04
  • Note that the second issue raised by @AndreyTyukin is only a problem for mutable types, whereas this `ListNode` implementation is immutable. – Tim Jun 28 '19 at 13:25