Implementing a method inside a Scala parameterized

2020-07-27 03:34发布

问题:

I've read a few tutorials including the main Scala documentation regarding method signatures of covariant types. Suppose I have the following abstract class:

abstract class List[+A] {

  def head: A
  def tail: List[A]
  def isEmpty: Boolean
  def add[B >: A](element: B): List[B]
  protected def printElements: String

  override def toString: String = "[" + printElements + "]"

}

My question concerns the signature of the add() method. Why is it necessary to declare it that way? We are passing in a parameter that is a supertype of A. What problem does this solve? I'm trying to understand this on an intuitive level.

回答1:

Formal explanation

Given

abstract class List[+A] {
  def add(element: A): List[A]
}

"This program does not compile, because the parameter element in add is of type A, which we declared covariant. This doesn’t work because functions are contravariant in their parameter types and covariant in their result types. To fix this, we need to flip the variance of the type of the parameter element in add.
We do this by introducing a new type parameter B that has A as a lower type bound".
-- reference.

Intuitive explanation

In this example, if you add something to a List:
It must be an A - in this case the List is still a List[A].
Or it must be any subtype of A - in this case the element gets upcasted to A, and the List remains a List[A].
Or if it is another type B, then it MUST be a supertype of A - in this case the List gets upcasted to a List[B]. (Note: Because Any is just a supertype of everything, in the worst case the List will be upcasted to List[Any]).



回答2:

Suppose I want to make a list of integers. And suppose, for the sake of argument, that add is implemented without generics.

def add(element: A): List[A]

For the sake of this example, let's suppose we have some way of producing an "empty" list.

def emptyList[A]: List[A] = /* some magic */

Now I want to make my list of integers.

(1 to 10).foldRight(emptyList) { (x, acc) => acc.add(x) }

Oops! We have a problem! When I call emptyList, Scala is going to infer the most general type, and since A is covariant, it's going to assume Nothing. That means I just tried to add an integer to a list of nothing. We could fix this problem with an explicit type signature,

(1 to 10).foldRight(emptyList[Int]) { (x, acc) => acc.add(x) }

But, really, that's not solving the problem. It adds nothing to readability and just requires the user to do extra work. Realistically, I should be able to append a number to a list of nothing. It's just that, if I choose to do so, I can't meaningfully call it a list of Nothing anymore. Hence, if we define

def add[B >: A](element: B): List[B]

Now, I can start with a List[Nothing] and add an Int to it. The thing I get out isn't a List[Nothing] anymore; it's a List[Int], but I can do it. If I take that List[Int] and come along later and add a String to it, well I can do that too, but now I have a virtually useless List[Any].



回答3:

When you declare +A, you're saying that, for example, List[String] extends List[Object]. Now, imagine this:

val ls: List[Object] = List[String]() // Legal because of covariance
ls.add(1) // Adding an int to a list of String?

This is only legal if the type of the List can be expanded to include arbitrary Objects, which is exactly what your add signature does. Otherwise, the existence of add(a: A) would imply an inconsistency in the type system.