EastLondonCoder
7 hours ago
After a 2 year Clojure stint I find it very hard to explain the clarity that comes with immutability for programmers used to trigger effects with a mutation.
I think it may be one of those things you have to see in order to understand.
rendaw
5 hours ago
I think the explanation is: When you mutate variables it implicitly creates an ordering dependency - later uses of the variable rely on previous mutations. However, this is an implicit dependency that isn't modeled by the language so reordering won't cause any errors.
With a very basic concrete example:
x = 7
x = x + 3
x = x / 2
Vs
x = 7
x1 = x + 3
x2 = x1 / 2
Reordering the first will have no error, but you'll get the wrong result. The second will produce an error if you try to reorder the statements.
Another way to look at it is that in the first example, the 3rd calculation doesn't have "x" as a dependency but rather "x in the state where addition has already been completed" (i.e. it's 3 different x's that all share the same name). Doing single assignment is just making this explicit.
raincole
37 minutes ago
Yet even Rust allows you to shadow variables with another one with the same name. Yes, they are two different variables, but for a human reader they have the same name.
I think that Rust made this decision because the x1, x2, x3 style of code is really a pain in the ass to write.
suspended_state
25 minutes ago
Or they got inspired by how this is done in OCaml, which was the host language for the earliest versions of Rust. Actually, this is a behaviour found in many FP languages. Regarding OCaml, there was even a experimental version of the REPL where one could access the different variables carrying the same name using an ad-hoc syntax.
jstimpfle
4 hours ago
The immutable approach doesn't conflate the concepts of place, time, and abstract identity, like in-place mutation does.
In mutating models, typically abstract (mathematical / conceptual) objects are modeled as memory locations. Which means that object identity implies pointer identity. But that's a problem when different versions of the same object need to be maintained.
It's much easier when we represent object identity by something other than pointer identity, such as (string) names or 32-bit integer keys. Such representation allows us to materialize us different versions (or even the same version) of an object in multiple places, at the same time. This allows us to concurrently read or write different versions of the same abstract object. It's also an enabler for serialization/deserialization. Not requiring an object to be materialized in one particular place allows saving objects to disk or sending them around.
EastLondonCoder
4 hours ago
I agree that the explicit timeline you get with immutability is certainly helpful, but I also think its much easier to understand the total state of a program. When an imperative program runs you almost always have to reproduce a bug in order to understate the state that caused it, fairly often in Clojure you can actually deduct whats happening.
ryandv
an hour ago
That's right - immutability enables equational reasoning, where it becomes possible to actually reason through a program just by inspection and evaluation in one's head, since the only context one needs to load is contained within the function itself - not the entire trace, where anything along the thread of execution could factor into your function's output, since anybody can just mutate anybody else's memory willy-nilly.
People jump ahead using AI to improve their reading comprehension of source code, when there are still basic practices of style, writing, & composition that for some reason are yet to be widespread throughout the industry despite already having a long standing tradition in practice, alongside pretty firm grounding in academics.
adrianN
13 minutes ago
In theory it’s certainly right that imperative programs are harder to reason about. In practice programmers tend to avoid writing the kind of program where anything can happen.
ryandv
2 minutes ago
> In practice programmers tend to avoid writing the kind of program where anything can happen.
My faith in this presumption dwindles every year. I expect AI to only exacerbate the problem.
Tarean
2 hours ago
Sometimes keeping a fixed shape for the variable context across the computation can make it easier to reason about invariants, though.
Like, if you have a constraint is_even(x) that's really easy to check in your head with some informal Floyd-Hoare logic.
And it scales to extracting code into helper functions and multiple variables. If you must track which set of variables form one context x1+y1, x2+y2, etc I find it much harder to check the invariants in my head.
These 'fixed state shape' situations are where I'd grab a state monad in Haskell and start thinking top-down in terms of actions+invariants.
skeezyjefferson
4 hours ago
whats the difference between immutable and constant, which has been in use far longer? why are you calling it mutable?
inanutshellus
4 hours ago
"Constant" implies a larger context.
As in - it's not very "constant" if you keep re-making it in your loop, right?
Whereas "immutable" throws away that extra context and means "whatever variable you have, for however long you have it, it's unchangeable."
skeezyjefferson
an hour ago
> As in - it's not very "constant" if you keep re-making it in your loop, right?
you cant change a constant though
Thorrez
4 hours ago
Immutable and constant are the same. rendaw didn't use the word mutable. One reason someone might use the word "mutable" is that it's a succinct way of expressing an idea. Alternative ways of expressing the same idea are longer words (changeable, non-constant).
a4isms
4 hours ago
In languages like JavaScript, immutable and constant may be theoretically the same thing, but in practice "const" means a variable cannot be reassigned, while "immutable" means a value cannot be mutated in place.
They are very, very different semantically, because const is always local. Declaring something const has no effect on what happens with the value bound to a const variable anywhere else in the program. Whereas, immutability is a global property: An immutable array, for example, can be passed around and it will always be immutable.
JS has always hade 'freeze' as a kind of runtime immutability, and tooling like TS can provide for readonly types that provide immutability guarantees at compile time.
everforward
2 hours ago
Arrays are a very notable example here. You can append to a const array in JS and TS, even in the same scope it was declared const.
That’s always felt very odd to me.
raddan
an hour ago
That's because in many languages there is a difference between a stored reference being immutable and the contents of the thing the reference points to being immutable.
skeezyjefferson
an hour ago
but we already had the word variable for values that can change. on both counts it seems redundant
kgwxd
4 hours ago
They aren't the same for object references. The reference can't be changed, but the properties can.
ape4
2 hours ago
I would be nicer if you gave x1 and x2 meaningful names
catlifeonmars
an hour ago
What would those names be in this example?
dwwoelfel
19 minutes ago
Carmack is talking about variable reassignment here, which Clojure will happily let you mutate.
For example:
(let [result {:a 1}
result (assoc result :b 2)]
...)
He mentions that C and C++ allow const variables, but Clojure doesn't support that.clj-kondo has a :shadowed-var rule, but it will only find cases where you shadow a top-level var (not the case in my example).
zelphirkalt
3 hours ago
Made a similar experience with Scheme. I could tell people whatever I wanted, they wouldn't really realize how much cleaner and easier to test things could be, if we just used functions instead of mutating things around. And since I was the only one who had done projects in an FP language, and they only used non-FP languages like Java, Python, JavaScript and TypeScript before, they would continue to write things based on needless mutation. The issue was also, that using Python it can be hard to write functional style code in a readable way too. Even JS seems to lend itself better to that. What's more is, that one will probably find oneself hard pressed to find the functional data structures one might want to use and needs to work around recursion due to the limitations of those languages.
I think it's simply the difference between the curious mind, who explores stuff like Clojure off the job (or is very lucky to get a Clojure job) and the 9 to 5 worker, who doesn't know any better and has never experienced writing a FP codebase.
emil0r
5 hours ago
The way I like to think about is that with immutable data as default and pure functions, you get to treat the pure functions as black boxes. You don't need to know what's going on inside, and the function doesn't need to know what's going on in the outside world. The data shape becomes the contract.
As such, localized context, everywhere, is perhaps the best way to explain it from the point of view of a mutable world. At no point do you ever need to know about the state of the entire program, you just need to know the data and the function. I don't need the entire program up and running in order to test or debug this function. I just need the data that was sent in, which CANNOT be changed by any other part of the program.
DrScientist
2 hours ago
Sure modularity, encapsulation etc are great tools for making components understandable and maintainable.
However, don't you still need to understand the entire program as ultimately that's what you are trying to build.
And if the state of the entire programme doesn't change - then nothing has happened. ie there still has to be mutable state somewhere - so where is it moved to?
maleldil
44 minutes ago
> there still has to be mutable state somewhere - so where is it moved to?
This is one way of thinking about it: https://news.ycombinator.com/item?id=45701901 (Simplify your code: Functional core, imperative shell)
jimbokun
36 minutes ago
> However, don't you still need to understand the entire program as ultimately that's what you are trying to build.
Of course not, that's impossible. Modern programs are way to large to keep in your head and reason about.
So you need to be able to isolate certain parts of the program and just reason about those pieces while you debug or modify the code.
Once you identify the part of the program that needs to change, you don't have to worry about all the other parts of the program while you're making that change as long as you keep the contracts of all the functions in place.
raddan
43 minutes ago
In functional programs, you very explicitly _do not_ need to understand an entire program. You just need to know that a function does a thing. When you're implementing a function-- sure, you need to know what it does. But you're defining it in such a way that the user should not know _how_ it works, only _what_ it does. This is a major distinction between programs written with mutable state and those written without. The latter is _much_ easier to think about.
I often hear from programmers that "oh, functional programming must be hard." It's actually the opposite. Imperative programming is hard. I choose to be a functional programmer because I am dumb, and the language gives me superpowers.
fwip
29 minutes ago
It's moved toward the edges of your program. In a lot of functional languages, places that can perform these effects are marked explicitly.
For example, in Haskell, any function that can perform IO has "IO" in the return type, so the "printLine" equivalent is: "putStrLn :: String -> IO". (I'm simplifying a bit here). The result is that you know that a function like "getUserComments :: User -> [CommentId]" is only going to do what it says on the tin - it won't go fetch data from a database, print anything to a log, spawn new threads, etc.
It gives similar organizational/clarity benefits as something like "hexagonal architecture," or a capabilities system. By limiting the scope of what it's possible for a given unit of code to do, it's faster to understand the system and you can iterate more confidently with code you can trust.
StopDisinfo910
2 hours ago
I think the advantage is often oversold and people often miss how things actually exist on a continuum and just plainly opposing mutable and immutable is sidestepping a lot of complexity.
For exemple, it's endlessly amusing to me to see all the efforts the Haskell community does to basically reinvent mutability in a way which is somehow palatable to their type system. Sometimes they even fail to even realise that it's what they are doing.
In the end, the goal is always the same: better control and warranties about the impact of side effects with minimum fuss. Carmack approach here is sensible. You want practices which make things easy to debug and reason about while mainting flexibility where it makes sense like iterative calculations.
pxc
20 minutes ago
If you read through the Big Red Book¹ or its counterpart for Kotlin², it's quite explicit about the goals with these techniques for managing effects, and goes over rewriting imperative code to manage state in a "pure" way.
I think the authors are quite aware of the relationship between these techniques and mutable state! I imagine it's similar for other canonical functional programming texts.
Besides the "pure" functional languages like Haskell, there are languages that are sort of immutability-first (and support sophisticated effects libraries), or at least have good immutable collections libraries in the stdlib, but are flexible about mutation as well, so you can pick your poison: Scala, Clojure, Rust, Nim (and probably lots of others).
All of these go further and are more comfortable than just throwing `const` or `.freeze` around in languages that weren't designed with this style in mind. If you haven't tried them, you should! They're really pleasant to work with.
----
1: https://www.manning.com/books/functional-programming-in-scal...
2: https://www.manning.com/books/functional-programming-in-kotl...
eyelidlessness
an hour ago
> Sometimes they even fail to even realise that it's what they are doing.
Because that’s not what they’re doing. They’re isolating state in a systemic, predictable way.
rafaelmn
2 hours ago
I would say it's more than immutability - it's the "feel" of working with values. I've worked with at least 6 languages professionally and likely more for personal projects over last 20 years. I can say that Clojure was the most impactful language I learned.
I tried to learn Haskel before but I just got bogged down in the type system and formalization - that never sat with me (ironically in retrospect Monads are a trivial concept that they obfuscated in the community to oblivion, yet another Monad tutorial was a meme at the time).
I used F# as well but it is too multi paradigm and pragmatic, I literally wrote C# in F# syntax when I hit a wall and I didn't learn as much about FP when I played with it.
Clojure had the lisp weirdness to get over, but it's homoiconicty combined with the powerful semantics of core data structures - it was the first time where the concept of working with values vs objects 'clicked' for me. I would still never use it professionally, but I would recommend it to everyone who does not have a background in FP and/or lisp experience.
ndr
3 hours ago
Clojure also makes it very easy, it'd require too much discipline to do such a thing in Python. Even Carmack, who I think still does python mostly by himself instead of a team, is having issues there.
ErroneousBosh
3 hours ago
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?
jimbokun
28 minutes ago
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.
jayd16
an hour ago
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.
mleo
3 hours ago
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.
ErroneousBosh
3 hours ago
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?
stickfigure
an hour ago
Sounds like you have a data structure like `Array<Float>`. The immutable approach has methods on Array like:
Array<Float> append(Float value);
Array<Float> replace(int index, Float value);
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.
m_rpn
6 hours ago
salutes from a WestLondonCoder