Hidden immutability

I thought that a val of an immutable type is always safely immutable, but it’s not. Try the code below:

    object A {
      var a:Int = 1
      def get = a
    }

    val map1 = Map("a" -> A)
    val map2:Map[String,Int] = map1.mapValues(_.get)
    val map3:Map[String,Int] = map1.map{ case (k,v) => (k,v.get) }.toMap

    println(s"${map2("a")} =?= ${map3("a")}")
    A.a = 2
    println(s"${map2("a")} =?= ${map3("a")}")

map2 and map3 are both vals of type Map[String,Int], with Map = scala.collection.immutable.Map. String and Int are immutable. Though map2 behaves mutable, due to the lazy behavior of .mapValues on Map.

Is this a feature, a trick, a trap or a bug in the language?

1 Like

Note A.a = 2 is not the output of the println above, but an assignment.
The output of the code is:

1 =?= 1
2 =?= 1

I’d say that it is simply a fact of life in a language that is partially OO.

Basically, you’ve just illustrated why var fields are generally dangerous. They’re absolutely normal, even idiomatic, in most object-oriented languages (and remember, Scala is a hybrid of OO and FP techniques), and they are occasionally useful. But they should only be used with considerable care, because (as you show above), it is easy for them to result in code that is difficult to reason about. I generally only use var fields in a few specific circumstances, when there is reason to believe the risk is contained.

I suppose you could call it a “trap”, in that it’s something to be quite careful with. But it’s absolutely intentional, and a point that I bring up pretty early when I’m teaching Scala.

You should think of map2 as a view on map1. The confusing thing is that its type is just a Map like map1. This is now fixed in Scala 2.13

This would have tripped me as I never realized that mapValues is indeed a view. The documentation is clear, though: a map view which maps every key of this map to f(this(key)). The resulting map wraps the original map without copying any elements.

Sounds good. I can live with the behaviour difference between map2 and map3 and with reasoning over it as a view, but I expect the type system to express this, showing the difference between the two.

Agree. It’s fixed in Scala 2.13:

scala 2.13.0-M5> Map(1 -> "foo").mapValues(s => "bar")
                                 ^
                 warning: method mapValues in trait MapOps is deprecated (since 2.13.0): Use .view.mapValues(f). A future version will include a strict version of this method (for now, .view.mapValues(f).toMap).
res0: scala.collection.MapView[Int,String] = <function1>

scala 2.13.0-M5> Map(1 -> "foo").view.mapValues(s => "bar")
res1: scala.collection.MapView[Int,String] = <function1>