57

E.g. why does

val list:List[Any] = List[Int](1,2,3)

work, but

val arr:Array[Any] = Array[Int](1,2,3)

fails (because arrays are invariant). What is the desired effect behind this design decision?

om-nom-nom
  • 62,329
  • 13
  • 183
  • 228
fresskoma
  • 25,481
  • 10
  • 85
  • 128
  • 8
    Note that java arrays are covariant, and this may cause a problem when calling java code from scala. – incrop Jul 14 '11 at 05:07
  • @incrop - can you please give an example? – Kevin Meredith Feb 23 '14 at 03:52
  • @KevinMeredith Integer[] source = {1, 2, 3}; Number[] target = source; target[0] = 4; target[0] = 3.14; // compiles // but ArrayStoreException at runtime //attempt of heap pollution – bridgemnc Sep 07 '22 at 13:27

4 Answers4

80

Because it would break type-safety otherwise. If not, you would be able to do something like this:

val arr:Array[Int] = Array[Int](1,2,3)
val arr2:Array[Any] = arr
arr2(0) = 2.54

and the compiler can't catch it.

On the other hand, lists are immutable, so you can't add something that is not Int

Simson
  • 3,373
  • 2
  • 24
  • 38
Op De Cirkel
  • 28,647
  • 6
  • 40
  • 53
  • 1
    You mean `Array`, not `List`, do you? With lists, your example won't work (no "update" method in type `List`). With `Array`s, it would be a valid counter example of what you could do, were arrays covariant. – Dirk Jul 13 '11 at 19:36
  • 1
    The credit should go to @sshannin, as i just put an example and rephrase what he said. – Op De Cirkel Jul 13 '11 at 20:04
  • why does `arr2(0)` evaluate to `2.54` in your example, @OpDeCirkel? – Kevin Meredith Jun 03 '14 at 17:20
  • @KevinMeredith, it is not an evaluation, it is the assignment. In the example if arrays in scala were not invariant, we would be apple to add floating point number (2.54 in case of example above) in an Int array. The example highlights the problem if array were not invariant. – Prashant Kalkar Jun 28 '14 at 18:03
  • @OpDeCirkel i think it's worth mentioning that another common immutable collection, `Set`, although safe to upcast, is also invariant in its type - this is because a `Set[A]` is also an `A => Boolean`, but functions are contravariant in their arguments. `Set`s can be upcasted either via `Set[Child]().asInstanceOf[Set[Parent]]`, or `Set[Child]().toSet[Parent]` - the first one smells but doesn't create a new collection. – Sergey May 25 '16 at 14:53
  • In Coursera's "Functional Programming Principles in Scala" there is a lecture that tackles precisely this question: Week 4 - Lecture 4.4 - Variance (Optional) This link will probably not be functional forever, but in case that someone checks this question soon: https://es.coursera.org/learn/progfun1/lecture/dnreZ/lecture-4-4-variance-optional – dieresys Aug 09 '17 at 17:05
25

This is because lists are immutable and arrays are mutable.

sshannin
  • 2,767
  • 1
  • 22
  • 25
  • 3
    The credit should go to @sshannin, as i just put an example and rephrase what he said. – Op De Cirkel Jul 13 '11 at 19:52
  • 23
    -1. Why do people upvote answers like this? Without some explanation of why this is relevant you might as well be saying "because `Array` starts with 'A' and `List` starts with 'L'". – Travis Brown Nov 10 '14 at 16:40
  • 1
    That's a good point, Travis. I upvoted it before I read your comment, and then realized I only upvoted it because I already knew the answer when I got here and this just articulated it concisely. But answers that only work if you already knew the answer aren't all that useful. – csjacobs24 May 13 '16 at 14:42
9

The normal answer to give is that mutability combined with covariance would break type safety. For collections, this can be taken as a fundamental truth. But the theory actually applies to any generic type, not just collections like List and Array, and we don't have to try and reason about mutability at all.

The real answer has to do with the way function types interact with subtyping. The short story is if a type parameter is used as a return type, it is covariant. On the other hand, if a type parameter is used as an argument type, it is contravariant. If it is used both as a return type and as an argument type, it is invariant.

Let's look at the documentation for Array[T]. The two obvious methods to look at are for the ones for lookup and update:

def apply(i: Int): T
def update(i: Int, x: T): Unit

In the first method T is a return type, while in the second T is an argument type. The rules of variance dictate that T must therefore be invariant.

We can compare the documentation for List[A] to see why it is covariant. Confusingly, we would find these methods, which are analogous to the methods for Array[T]:

def apply(n: Int): A
def ::(x: A): List[A]

Since A is used as both a return type and as an argument type, we would expect A to be invariant just like T is for Array[T]. However, unlike with Array[T], the documentation is lying to us about the type of ::. The lie is good enough for most calls to this method, but isn't good enough to decide the variance of A. If we expand the documentation for this method and click on "Full Signature", we are shown the truth:

def ::[B >: A](x: B): List[B]

So A does not actually appear as an argument type. Instead, B (which can be any supertype of A) is the argument type. This does not place any restriction on A, so it really can be covariant. Any method on List[A] which has A as an argument type is a similar lie (we can tell because these methods are marked as [use case]).

Ken Wayne VanderLinde
  • 18,915
  • 3
  • 47
  • 72
  • 1
    You did not explain why "The lie is good enough for most calls to this method", and you did not explain why they chose not to use this lie for Array and make it also covariant. – rapt Oct 05 '18 at 03:05
  • 1
    If I could +1 this more than once ... the part about the real signature for `::` was priceless – Ashkan Kh. Nazary Dec 07 '20 at 13:05
  • @rapt there was a decision from `2.8` onward to spare the reader from the nuances of type annotations in collection API and just show the gentler signature that covers the vast majority of use cases. That's why it says `...[A]` and not `...[B >: A]` *only* in the docs and *only* as an aid to be a gentler read. This doesn't mean the actual method signature is the simpler one. The actual method signature is still the more complex one with the correct variance. – Ashkan Kh. Nazary Dec 07 '20 at 13:10
7

The difference is that Lists are immutable while Arrays are mutable.

To understand why mutability determines variance, consider making a mutable version of List - let's call it MutableList. We'll also make use of some example types: a base class Animal and 2 subclasses named Cat and Dog.

trait Animal {
  def makeSound: String
}

class Cat extends Animal {
  def makeSound = "meow"
  def jump = // ...
}

class Dog extends Animal {
  def makeSound = "bark"
}

Notice that Cat has one more method (jump) than Dog.

Then, define a function that accepts a mutable list of animals and modifies the list:

def mindlessFunc(xs: MutableList[Animal]) = {
  xs += new Dog()
}

Now, horrible things will happen if you pass a list of cats into the function:

val cats = MutableList[Cat](cat1, cat2)
val horror = mindlessFunc(cats)

If we were using a careless programming language, this will be ignored during compilation. Nevertheless, our world will not collapse if we only access the list of cats using the following code:

cats.foreach(c => c.makeSound)

But if we do this:

cats.foreach(c => c.jump)

A runtime error will occur. With Scala, writing such code is prevented, because the compiler will complain.

Ken Wayne VanderLinde
  • 18,915
  • 3
  • 47
  • 72
Yuhuan Jiang
  • 2,616
  • 2
  • 19
  • 21
  • This doesn't answer the question, and in fact doesn't mention arrays at all. The OP might infer that the problem comes from the mutability of arrays, but it isn't stated here. – csjacobs24 May 13 '16 at 14:44
  • 1
    @csjacobs24 I have added a line at the top of the answer to provide a direct answer to the question. The purpose of the original answer was to explain why it is wrong for mutable lists to be covariant. – Yuhuan Jiang Mar 07 '17 at 20:29
  • @YuhuanJiang Nice answer , it can be better it you expand it to explain how the problem doesn't happen when list is immutable(default). Also at the beginning of the answer you can start with explaining what covariance really is for the folks who do not know along with the liskov principle. None of the answers here to this important question are comprhensive. – Rpant Jun 04 '17 at 04:49