Ease of Minor Updates
One reason to use mutability is if you're keeping track of some ongoing process. For example, let's suppose I am editing a large document and have a complex set of classes to keep track of the various elements of the text, the editing history, the cursor position, and so on. Now suppose the user clicks on a different part of the text. Do I recreate the document object, copying many fields but not the EditState
field; recreate the EditState
with new ViewBounds
and documentCursorPosition
? Or do I alter a mutable variable in one spot? As long as thread safety is not an issue then is is much simpler and less error-prone to just update a variable or two than to copy everything. If thread safety is an issue, then protecting from concurrent access may be more work than using the immutable approach and dealing with out-of-date requests.
Computational efficiency
Another reason to use mutability is for speed. Object creation is cheap, but simple method calls are cheaper, and operations on primitive types are cheaper yet.
Let's suppose, for example, that we have a map and we want to sum the values and the squares of the values.
val xs = List.range(1,10000).map(x => x.toString -> x).toMap
val sum = xs.values.sum
val sumsq = xs.values.map(x => x*x).sum
If you do this every once in a while, it's no big deal. But if you pay attention to what's going on, for every list element you first recreate it (values), then sum it (boxed), then recreate it again (values), then recreate it yet again in squared form with boxing (map), then sum it. This is at least six object creations and five full traversals just to do two adds and one multiply per item. Incredibly inefficient.
You might try to do better by avoiding the multiple recursion and passing through the map only once, using a fold:
val (sum,sumsq) = ((0,0) /: xs){ case ((sum,sumsq),(_,v)) => (sum + v, sumsq + v*v) }
And this is much better, with about 15x better performance on my machine. But you still have three object creations every iteration. If instead you
case class SSq(var sum: Int = 0, var sumsq: Int = 0) {
def +=(i: Int) { sum += i; sumsq += i*i }
}
val ssq = SSq()
xs.foreach(x => ssq += x._2)
you're about twice as fast again because you cut the boxing down. If you have your data in an array and use a while loop, then you can avoid all object creation and boxing and speed up by another factor of 20.
Now, that said, you could also have chosen a recursive function for your array:
val ar = Array.range(0,10000)
def suma(xs: Array[Int], start: Int = 0, sum: Int = 0, sumsq: Int = 0): (Int,Int) = {
if (start >= xs.length) (sum, sumsq)
else suma(xs, start+1, sum+xs(start), sumsq + xs(start)*xs(start))
}
and written this way it's just as fast as the mutable SSq. But if we instead do this:
def sumb(xs: Array[Int], start: Int = 0, ssq: (Int,Int) = (0,0)): (Int,Int) = {
if (start >= xs.length) ssq
else sumb(xs, start+1, (ssq._1+xs(start), ssq._2 + xs(start)*xs(start)))
}
we're now 10x slower again because we have to create an object on each step.
So the bottom line is that it really only matters that you have immutability when you cannot conveniently carry your updating structure along as independent arguments to a method. Once you go beyond the complexity where that works, mutability can be a big win.
Cumulative Object Creation
If you need to build up a complex object with n
fields from potentially faulty data, you can use a builder pattern that looks like so:
abstract class Built {
def x: Int
def y: String
def z: Boolean
}
private class Building extends Built {
var x: Int = _
var y: String = _
var z: Boolean = _
}
def buildFromWhatever: Option[Built] = {
val b = new Building
b.x = something
if (thereIsAProblem) return None
b.y = somethingElse
// check
...
Some(b)
}
This only works with mutable data. There are other options, of course:
class Built(val x: Int = 0, val y: String = "", val z: Boolean = false) {}
def buildFromWhatever: Option[Built] = {
val b0 = new Built
val b1 = b0.copy(x = something)
if (thereIsAProblem) return None
...
Some(b)
}
which in many ways is even cleaner, except you have to copy your object once for each change that you make, which can be painfully slow. And neither of these are particularly bulletproof; for that you'd probably want
class Built(val x: Int, val y: String, val z: Boolean) {}
class Building(
val x: Option[Int] = None, val y: Option[String] = None, val z: Option[Boolean] = None
) {
def build: Option[Built] = for (x0 <- x; y0 <- y; z0 <- z) yield new Built(x,y,z)
}
def buildFromWhatever: Option[Build] = {
val b0 = new Building
val b1 = b0.copy(x = somethingIfNotProblem)
...
bN.build
}
but again, there's lots of overhead.