Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I think this is a good idea and something that has been badly misunderstood by probably the majority of the community for a long time. Consider the following code in an unspecified language:

    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.



'You can't have a thunk in such a language that may end up either one thing or another, "depending".'

Well, you can, and on a very rare occasion that's what you want (see "amb"), but you should be explicit that that's what you're asking for (and probably why) because those occasions really are pretty rare and usually pretty complicated/subtle.


Simplifications, of course. :) See also "lazy IO" and the joys of "seq" and all the manifold subtleties it has.

But I'll stick to my original claim, modified by "in general"... in general, nondeterministic thunks (where "nondeterminism" means "influenced by things that are not immutably set at the function's start of execution" or something like that) is dangerous and almost (but not quite) always wrong.


For sure. I didn't mean it as a gotcha, just raising an interesting (I hope) aside.


Yes, it has no effect on "correctness". The two are equivalent. But your example is nonsensical. If you scale this up to non-trivial problems I have a hunch we'd find this pervasively affects the design losing the ability to compose functions. It implies an ordering on the "dependencies/ownership" of the program.

Once you write your program, if a program is fully SSA you are right it is equivalent to an immutable one. However this affects how one designs the software in the first place. You would not write the code the same with the different tool-sets.

I have struggled with this because working in Haskell taking Rusts approach seems very appealing as it allows mutability. But every time I just come to the conclusion that it is a bad idea as it will end up greatly affecting modularity and composability.

To give an example. If I have a pure function A, which now calls function B which mutates a value, function A is no longer pure. Now you lose the ability to compose that function in many ways. This would happen all over the program.


If I'm reading your post correctly, what you're calling "nonsensical" is deliberately part of my point; you may want to re-read my post in light of that fact. The difference between a local context and a fully-composed program in a conventional mutable language is a core part of my argument, not something I overlooked.


What I am saying is mutability is not something that tends to stay local. Either you isolate it at the top, ala Haskell or it tends to find it's way into everything.

SSA is great, except that as you say it is functionally equivalent to immutability. There is no advantage to it over using immutable data structures. In fact, it is inferior as it only covers a subset of problems that immutability does.

The examples in the blog post illustrate this. He wants to make local variables mutable because otherwise he can't pass a reference to a function which mutates a value. That mutation is completely unnecessarily and could of been performed with a pure function instead.

Note the blog author says: "Think: when was the last time you responded to a compiler error about illegal mutation by doing anything other than restructuring the code to make the mutation legal?"

I actually do this all the time. That is probably the largest challenge coming to a language like Haskell from an imperative background. Sometimes you can't just fix the little mutability problem locally, it has to be redesigned with immutability in mind.


I think what you're actually talking about is really the desire for Referential Transparency[0] vs. the mutable/immutable distinction.

I certainly agree that, to some degree (as long as you have some way of guaranteeing referential transparency) then the default for local variables being mutable/immutable doesn't really matter. The important thing is the RT bit, and if Rust can guarantee that (via no-aliasing or linear types or whatever) then it's already ahead of the bunch in terms of safety.

Of course mutability can prevent algebraic reasoning and trivial reuse via simple algebraic extraction of code/functions, but so can strictness, so...

[0] http://en.wikipedia.org/wiki/Referential_transparency_(compu...


"I think what you're actually talking about is really the desire for Referential Transparency[0] vs. the mutable/immutable distinction."

I don't think so... in the arrangement I'm talking about nothing stops you from getting input from an arbitrary source. I suppose this implies there should only be one witness to the change at a time, but that's not all that hard of a restriction.

"Of course mutability can prevent algebraic reasoning and trivial reuse via simple algebraic extraction of code/functions, but so can strictness, so..."

Indeed... if we're going to write off a class of useful things, let us make sure we get as much benefit from it as possible. And I can't imagine Rust dropping default-strict anytime soon... in the domain it seeks to conquer it is arguably the correct choice even from a Haskell perspective. Haskell lacks that truly low-level systems orientation.


Input from an arbitrary source (i.e. without referential transparency) implies that there can be no equational reasoning, almost by definition. This is kind of the case for IO in Haskell[0], but for subprograms/procedures/functions I tend to find it an absolutely essential property, both in terms of reasoning and in terms of testability. For example, it's the whole reason that QuickCheck works.

[0] Modeling IO as a World->World function doesn't quite work.


The functional programming guidelines I've seen (including, for example, Paul Graham's "On Lisp") have in effect said that if you need to mutate local variables you own, that's no big deal, as long as you can encapsulate that within one function. As far as consumers of that function are concerned, it is "pure," regardless of the fact that it uses mutation within its definition.

That much seems fairly obvious to me, but I'm somewhat new to functional programming. Is that all that is at stake here?


Less pathologically, loops and conditionals also expose mutability, hence phi nodes.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: