I guess I'm not that good a programmer, because I don't really understand why variables that can't be varied are useful, or why you'd use that.
How do you write code that actually works?
I guess I'm not that good a programmer, because I don't really understand why variables that can't be varied are useful, or why you'd use that.
How do you write code that actually works?
The concept is actually pretty simple: instead of changing existing values, you create new values.
The classic example is a list or array. You don't add a value to an existing list. You create a new list which consists of the old list plus the new value. [1]
This is a subtle but important difference. It means any part of your program with a reference to the original list will not have it change unexpectedly. This eliminates a large class of subtle bugs you no longer have to worry about.
[1] Whether the new list has completely new copy of the existing data, or references it from the old list, is an important optimization detail, but either way the guarantee is the same. It's important to get these optimizations right to make the efficiency of the language practical, but while using the data structure you don't have to worry about those details.
> The classic example is a list or array. You don't add a value to an existing list. You create a new list which consists of the old list plus the new value. [1]
Getting back to this, though - where would this be useful? What would do this?
I'm not getting why having a new list that's different from the old list, with some code working off the old list and some working off the new list, is anything you'd ever want.
Can you give a practical example of something that uses this?
Why doesn't the list just have a mutex?
> It means any part of your program with a reference to the original list will not have it change unexpectedly.
I don't get why that would be useful. The old array of floats is incorrect. Nothing should be using it.
That's the bit I don't really understand. If I have a list and I do something to it that gives me another updated list, why would I ever want anything to have the old incorrect list?
You pass in an array to a function meant to perform a transformation on each item of the array and return the result.
You pass in an array of 10 values.
While the function is executing, some other thread adds two more values to the array.
How many values should the result of the function call have? 10 or 12? How do you guarantee that is the case?
> While the function is executing, some other thread adds two more values to the array.
This is not something that can happen.
Why not? Which language?
Well, the stuff I'm writing is in C, but in general it would make no sense for anything to attempt to add items to a fixed-sized buffer.
If you have something so fundamentally broken as to attempt that, you'd probably want to look at mutexes.
Why one earth would you have something attempt to expand a fixed-sized buffer while something else is working on it?
There’s a mismatch between your assumptions coming from C and GP’s assumptions coming from a language where arrays are not fixed-length. Having a garbage collector manage memory for you is pretty fundamental to immutable-first languages.
Rich Hickey asked once in a talk, “who here misses working with mutable strings?” If you would answer “I do,” or if you haven’t worked much in languages where strings are always immutable and treated as values, it makes describing the benefits of immutability more challenging.
Von Neumann famously thought Assembly and higher-level language compilers were a waste of time. How much that opinion was based on his facility with machine code I don’t know, but compilers certainly helped other programmers to write more closely to the problem they want to solve instead of tracking registers in their heads. Immutable state is a similar offloading-of-incidental-complexity to the machine.
I must admit I do regard assembly language with some suspicion, because the assembler can make some quite surprising choices. Ultra-high-level languages like C are worse, though, because they can often end up doing things like allocating really wacky bits of memory for variables and then having to get up to all sorts of stunts to index into your array.
State exists in time, a variable is usually valid at the point it's created but it might not be valid in the future. Thus if part of your program accesses a variable expecting it to be from a certain point in time but it's actually from another point in time (was mutated) that can cause issues.
If you need new values you just make new things.
If you want to do an operation on fooA, you don't mutate fooA. You call fooB = MyFunc(fooA) and use fooB.
The nice thing here is you can pass around pointers to fooA and never worry that anything is going to change it underneath you.
You don't need to protect private variables because your internal workings cannot be mutated. Other code can copy it but not disrupt it.
> If you want to do an operation on fooA, you don't mutate fooA. You call fooB = MyFunc(fooA) and use fooB.
This is the bit I don't get.
Why would I do that? I will never want a fooA and a fooB. I can't see any circumstances where having a correct fooB and an incorrect fooA kicking around would be useful.
It is about being able to think clearly about your code logic. If your code has many places where a variable can change, then it is hard to go back and understand exactly where it changed if you have unexpected behavior. If the variable can never change then the logical backtrace is much shorter.
As Carmack points out, naming the intermediate values aides in debugging. It also helps you write code as you can give a name to every mutation.
But also keep in mind that correct and incorrect is not binary. You might want to pass a fooA to another class that does not want the fooB mutation.
If you just have foo, you end up with situations where a copy should have happened but didn't and then you get unwanted changes.
But that's just it, why would a copy ever happen? Why would you want a correct and an incorrect version of your variable hanging about?
Taking your point of view: you assigned a value1 to a name. Then you assigned a value2 to the same name.
You say that value2 is correct. It logically follows that value1 was incorrect. Why did you assign it then?
The names are free, you can just use a correct name every single time.
Because the account owner withdrew money . The player scored a goal, the month ticked over, the rain started, the car accelerated, a new comment was added to the thread .
The world by definition mutates over time.
Ah, true. If the var is a part of a long-living state, all good. That's just rarely seen in CRUD apps, more common in games.
> If you need new values you just make new things. > If you want to do an operation on fooA, you don't mutate fooA. You call fooB = MyFunc(fooA) and use fooB.
The beautiful thing about this is you can stop naming things generically, and can start naming them specifically what they are. Comprehension goes through the roof.
Yes, but you can do that without having loads of stale copies of incorrect data lying around, presumably.
It forces you to consider when, where and why a change occurs and can help reason later about changes. Thread safety is a big plus.
Okay, so for example I might set something like "this bunch of parameters" immutable, but "this 16kB or so of floats" are just ordinary variables which change all the time?
Or then would the block of floats be "immutable but not from this bit"? So the code that processes a block of samples can write to it, the code that fills the sample buffer can write to it, but nothing else should?
Sounds like you have a data structure like `Array<Float>`. The immutable approach has methods on Array like:
The methods don't mutate the array, they return a new array with the change.The trick is: How do you make this fast without copying a whole array?
Clojure includes a variety of collection classes that "magically" make these operations fast, for a variety of data types (lists, sets, maps, queues, etc). Also on the JVM there's Vavr; if you dig around you might find equivalents for other platforms.
No it won't be quite as fast as mutating a raw buffer, but it's usually plenty fast enough and you can always special-case performance sensitive spots.
Even if you never write a line of production Clojure, it's worth experimenting with just to get into the mindset. I don't use it, but I apply the principles I learned from Clojure in all the other languages I do use.
> The methods don't mutate the array, they return a new array with the change.
But then I need to update a bunch of stuff to point to the new array, and I've still got the old incorrect array hanging around taking up space.
This just sounds like a great way to introduce bugs.
It ends up being quite the opposite - many, many bugs come from unexpected side effects of mutation. You pass that array to a function and it turns out 10 layers deeper in the call stack, in code written by somebody else, some function decided to mutate the array.
Immutability gives you solid contracts. A function takes X as input and returns Y as output. This is predictable, testable, and thread safe by default.
If you have a bunch of stuff pointing at an object and all that stuff needs to change when the inner object changes, then you "raise up" the immutability to a higher level.
If you keep going with this philosophy you end up with something roughly like "software transactional memory" where the state of the world changes at each step, and you can go back and look at old states of the world if you want.Old states don't hang around if you don't keep references to them. They get garbage collected.
Okay, so this sounds like it's a method of programming that is entirely incompatible with anything I work on.
What sort of thing would it be useful for?
The kind of things I do tend to have maybe several hundred thousand floating point values that exist for maybe a couple of hundred thousandths of a second, get processed, get dealt with, and then are immediately overwritten with the next batch.
I can't think of any reason why I'd ever need to know what they were a few iterations back. That's gone, maybe as much as a ten-thousandth of a second ago, which may as well be last year.
It is useful for the vast majority of business processing. And, if John Carmack is to be believed, video game development.
Carmamack's post explains it - if you make a series of immutable "variables" instead of reassigning one, it is much easier to debug. This is a microcosm of time travel debugging; it lets you look at the state of those variables several steps back.
In don't know anything about your specific field but I am confident that getting to the point where you deeply understand this perspective will improve your programming, even if you don't always use it.