var int x
x = 1
x = 2
print(x)
x = 3
Clearly, "x" is a mutable variable. However, in isolation, this is not really true. We've long known about "Single static assignment" (SSA, [1]) and the fact that modulo memory issues, this ss equivalent to the above: var int x1 = 1
var int x2 = 2
print(x2)
var int x3 = 3
(Obviously the next thing the compiler will observe is unused variables. Bear with me on that so I can keep the code samples simple. :) )So in the context of a single simple executable context like this, the distinction between mutable and immutable is actually meaningless. Again modulo possible memory consumption issues, there's nothing you can write that will actually distinguish between "being in an immutable environment simulating mutability" (post-SSA transform) or "being in a mutable environment" in this single, small execution environment. If you can not distinguish between the two, then there is no difference.
Let me highlight this again. The existence of this transform is not merely an interesting trivia bit that happens to make optimizing compilers easier to write. It is a fundamental mathematical statement about the relationship between mutability and immutability at this particular scale. This is an important truth.
When does mutability matter? Well, maximally pathologically, with something like this:
var int x
x = 1
spawn_a_new_thread(fun() {
x = 2
})
print(x)
Now what happens? Obviously now we have a way of "witnessing" mutability vs. immutability (at least on a probabilistic basis).Less obviously, in a mutable language with references mutability still allows "action at a distance", where a function holds a reference to some value X, calls another function with Y, and doesn't "realize" that function will also as a side effect update X. In theory, you could transform this to a purely immutable representation, but in practice, this is the same problem as in the threading case... lighter weight since now it's deterministic and that makes it much easier to deal with than the threading case, but it still explodes the complexity of the program.
The crime of "mutability" is not actually the mutability per se. (After all, under the hood it's all just RAM anyhow... if mutability was fundamentally bad, we'd have already lost!) The crime is performing mutations on the context of a function without the function being aware of it. Threading makes it much worse, but even in a single-threaded context this can produce disaster. So the goal should be to ensure that functions can be assured that all the context they know about won't change without their knowledge and/or consent (to continue my anthropomorphization). More mathematically, it's really hard to prove (formally or just to yourself) any particular invariant in an environment where really any time you release control (single threaded) or even just whenever (multithreaded) things you "own" may be changed out from underneath you... in the worst case, composite data structures can change while you use them! The function becomes practically nondeterministic, and that's hard to work with.
A way to do this is by ensuring that everything is immutable, but for this particular purpose that's overkill. Ensuring that only things you uniquely own can be mutated works just fine... you own it, you "know" you changed it, and you've proved that nobody else can be surprised by any changes, even within the same thread, then you're back where we started at the beginning... you're a simple transform away from "immutable", and despite multithreading, the function is again deterministic.
Erlang has always struck me as suffering from this particular badly... the coding style often ends up with Value1, Value2, Value3 as we perform transforms on some value, but Value1 is immutable so we can't "update" it, but even if we could, it wouldn't matter because there's already no references in the language, so there's already no way to send these things across processes. The immutability is really just an annoying side show [2], what matters is that the language has strictly value-only semantics for inter-process communication. It makes the language significantly more annoying to program in for what is basically no gain, because back in the 90s this confusion about what immutability is really for was quite prevalent.
This is the key to writing simple, yet robust code; for a function to know that once it starts, there are no surprises. Whatever state it witnesses at the beginning will not change out from underneath it, so we need not continuously be screwing around with locks or fearing what other code may have references to our values.
Incidentally, true pervasive immutability becomes important if one is also lazy. You can't have a thunk in such a language that may end up either one thing or another, "depending". The nature of a "execution context" in such a language is very different and the line between functions becomes much fuzzier, especially with multithreading permitting true nondeterminism of thunk evaluation order. In a strict language, though, I think immutability is a side-show, what matters is ownership and change visibility.
[1]: http://en.wikipedia.org/wiki/Static_single_assignment_form
[2]: And yes, I also know that "=" is not "assign" but "pattern match with binding", but we could trivially add a := "pattern match and re-bind" operator to the compiler and the VM would never even have to know.