Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

relation between variance and mutabilty / immutability in Scala

Tags:

scala

I am playing around with collections in Scala and found that mutable collections are defined as invariant and immutable collections are defined as covariant. What is the relation between variance and mutability / immutability in Scala?

class Array[T]

class List[+T]
like image 879
nish1013 Avatar asked Sep 01 '13 16:09

nish1013


2 Answers

I had found an easy explanation in SIA. Following is straight from there.

Mutable objects need to be invariant A type parameter is invariant when it’s neither covariant nor contravariant. All Scala mutable collection classes are invariant. An example can explain why mutable objects need to be invariant. Because ListBuffer is mutable, it’s declared as invariant as follows:

final class ListBuffer[A] ...{ ... }

Because it’s declared as invariant, you can’t assign ListBuffer from one type to another. The following code will throw a compilation error:

scala> val mxs: ListBuffer[String] = ListBuffer("pants")
 mxs: scala.collection.mutable.ListBuffer[String] =
          ListBuffer(pants)
scala> val everything: ListBuffer[Any] = mxs
<console>:6: error: type mismatch;
found   : scala.collection.mutable.ListBuffer[String]
required: scala.collection.mutable.ListBuffer[Any]
     val everything: ListBuffer[Any] = mxs

Even though String is a subtype of scala.Any, Scala still doesn’t let you assign mxs to everything. To understand why, assume ListBuffer is covariant and the following code snippet works without any compilation problem:

scala> val mxs: ListBuffer[String] = ListBuffer("pants")
  mxs: scala.collection.mutable.ListBuffer[String] =
         ListBuffer(pants)
  scala> val everything: ListBuffer[Any] = mxs
  scala> everything += 1
  res4: everything.type = ListBuffer(1, pants)

Can you spot the problem? Because everything is of the type Any, you can store an integer value into a collection of strings. This is a disaster waiting to happen. It’s exactly what happens to Java arrays. To avoid these kinds of problems, it’s always a good idea to make mutable objects invariant. The next question is what happens in case of an immutable object for collections. It turns out that for immutable objects, covariance isn’t a problem at all. If you replace ListBuffer with the immutable List, you can take an instance of List[String] and assign it to List[Any] with- out a problem.

scala> val xs: List[String] = List("pants")
xs: List[String] = List(pants)
scala> val everything: List[Any] = xs
everything: List[Any] = List(pants)

The only reason this assignment is safe is because List is immutable. You can add 1 to xs List, and it will return a new List of type Any.

scala> 1 :: xs
res5: List[Any] = List(1, pants)

Again, this addition is safe because the cons(::) method always returns a new List, and its type is determined by the type of elements in the List. The only type that could store an integer value and reference value is scala.Any. This is an important property to remember about type variance when dealing with mutable/ immutable objects.

The best way to understand contravariance is to see the problem that comes when it’s absent. Try to spot the problem in the following Java code example:

Object[] arr = new int[1];
arr[0] = "Hello, there!";

You end up assigning the string to an integer array. Java catches this error at runtime by throwing an ArrayStoreException. Scala stops these kinds of errors at compile time by forcing parameter types to be either contravariant or invariant.

Hope this helps.

like image 115
Vikas Pandya Avatar answered Sep 24 '22 23:09

Vikas Pandya


A type can only be marked covariant if that type parameter only appears in covariant positions. Generally, this means that the class/trait/object has methods that returns values of the variant type but does not have methods with parameters of the variant type. Mutable collections always have methods with parameters of the variant type, e.g. update. Imagine what would happen if Array could be declared Array[+T]:

val as = Array[String]("a string")

// this statement won't typecheck in actual Scala
val aa: Array[AnyRef] = as

aa(0) = ("I'm a string...", "but this tuple itself isn't!")

// Tuples don't have a substring method, so this would fail at run-time
// if the compiler allowed this code to compile.
as(0).substring(0)
like image 33
wingedsubmariner Avatar answered Sep 25 '22 23:09

wingedsubmariner