5

I am studying variance in scala right now, and I think I have a good understanding of contravariance. For example given trait List[-A], I know that List[Int] is a supertype of List[AnyVal].

But say that I have the following trait:

trait List[+A] {
  def cons(hd: A): List[A]
}

Why is cons parameter type wrong?

Why it is necessary to have def cons[B >: A](v: B): List[B] ?

For example:

val animal_list: List[Animal] = List(tiger, dog)

if we call:

animal_list.cons(tiger)

since Tiger <: Animal, doesn't cons ran into problem? Since B is Tiger and A is Animal and B >: A is not true.

testing
  • 2,303
  • 4
  • 20
  • 32

1 Answers1

6

Why is cons's parameter type wrong?

trait List[+A] {
  def cons(hd: A): List[A]
}

Compiler give you error:
covariant type A occurs in contravariant position in type A of value hd
because method parameters count as contravariant positions, but A is covariant.

Let's imagine that this method declaration would compile. Then we could do:

class ListImpl[A] extends List[A] {
  override def cons(hd: A): List[A] = ???
}

val strings: List[String] = new ListImpl[String]
val values: List[Any] = strings // OK, since List[String] <: List[Any] (in List[A], A is covariant)
values.cons(13) // OK(??), since values's static type is List[Any], so argument of cons should be Any, and 13 conforms to type Any

Is the last line above really OK? We are calling cons on values. values is the same as strings, and strings is object of type ListImpl[String]. So cons invocation in the last line is expecting String argument, but we are passing Int, because values's static type is List[Any] and Int conforms to Any. Something is definitely wrong here - which line is to blame? The answer is: cons method declaration. To fix this issue, we have to remove covariant type parameter A from contravariant position (in cons declaration). Alternatively we can make A non-covariant.

See also these questions: #1, #2.

... doesn't cons ran into problem?

trait List[+A] {
  def cons[B >: A](v: B): List[B]
}

val animal_list: List[Animal] = List(tiger, dog)  // We are assuming that List.apply and concrete implementation of List is somewhere defined.

No, animal_list.cons(tiger) invocation is type-correct.

I assume that Animal is common supertype of Dog and Tiger, and that dog and tiger are instances of Dog and Tiger respectively.

In animal_list.cons(tiger) invocation, both A and B type parameters are instantiated to Animal, so cons method takes form of:

def cons[Animal >: Animal](v: Animal): List[Animal]

Animal >: Animal constraint is satisfied because:

Supertype and subtype relationships are reflexive, which means a type is both a supertype and a subtype of itself. [source]

The argument to cons is Tiger which conforms to type Animal, so the method invocation is type-correct.

Notice that if you force B to be instantiated to Tiger, like animal_list.cons[Tiger](tiger), then this invocation won't be type-correct, and you'll get compiler error.

See similar example here.

Community
  • 1
  • 1
TeWu
  • 5,928
  • 2
  • 22
  • 36
  • Oh I see, so the reason that `Tiger` was able to be passed to `cons` despite `Tiger <: Animal` is because `Tiger` get typecast to `Animal` first? – testing May 20 '16 at 17:42
  • @testing - With declaration `def cons[B >: A](v: B): List[B]` you can pass object of any type to `cons` - type argument `B` will always be instantiated to the most precise common supertype of `A` and the type of `cons`'s argument. Passing object to method which expects supertype argument is normal OO thing - e. g. you can always pass `Tiger` when `Animal` is expected. – TeWu May 20 '16 at 18:38
  • What if I have `val tiger_list: List[Tiger] = List(tiger)` this would imply `tiger_list.con(animal)` is valid? shouldn't we have something like `def cons[B >: A][C <: A](v: B): List[C]` – testing May 23 '16 at 23:27
  • 1
    @testing - When you call `List(tiger).cons(animal)` then `B` will be instantiated to the most precise common supertype of `Tiger` and `Animal`, which is `Animal`. When you call `List(tiger).cons(7)` then `B` will be instantiated to `Any`, because it is the most precise common supertype of `Tiger` and `Int`. If you are still confused, then run this code in REPL, and see what `cons` returns for different types of arguments. – TeWu May 30 '16 at 18:19