After a long long time…
Immutability also Empowers Type Safety.
There are many reasons to use immutable data structures and you may have heard many of them. For example, immutable data is safe to be shared across multiple threads etc. Apart from all of these awesome benefits, immutability helps us to employ better type systems. Here is an example:
In Java, Lists are not covariant. What does that mean? Long story short, List<Cat>
is not subtype of List<Animal>
. If they were, the following code would bark at runtime:
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}
List cats = new ArrayList()
List animals = cats;
animals.add(new Dog());
Cat persian = cats.get(0); // DOG IS NOT CAT!!!!
Lists (and other containers) not being covariant makes our lives hard. For example, if I have a function that accept List<Animal>
, I cannot pass List<Cat>
to it. I have to use wildcard and I lose type safety.
In contrast, in Scala, Lists
are covariant. So, the following code will compile:
val cats: List[Cat] = List(new Cat(), new Cat())
val animals: List[Animal] = cats
Why is Scala too brave to let you do that? Because you cannot change animals after declaring it and that is why, in Scala (and Kotlin), only immutable collections are covariant. So you cannot add any Dog to animals! In other word, immutable list does not expose any function that consumes an instance of T
. if it did, the following case would happen:
class List[+T] {
def consume(a: T, int index): Unit {
underlying[index] = a
}
}
val cats = List[Cat] = List(new Cat(), new Cat())
val animals: List[Animal] = cats
animals.consume(new Dog, 0) // change the first cat to a dog!!!
Instead, instances of T
are always produced by List
member functions. Fancy speaking, instances of T
are always used in covariant position and not in contravariant position. Even if you want to define your covariant typed class where there is a member that consumes an instance of type parameter, it will fail to compile.
class MyClass[+A] {
def set(a: A): Unit = {} // fails to compile as an instance of A is used in a contravariant position
}
class MyClass[+A] {
def get(): A = ... // it compiles, type parameter is used in covariant position
}
Kotlin has a similar approach as Scala, but what I like more in Kotlin (only in this very specific case) is that, to define covariant or contravariant classes, instead of using +
and -
, it uses out
and in
which clearly emphasize on producing and consuming.