Skip to content(if available)orjump to list(if available)

John Carmack on mutable variables

John Carmack on mutable variables

115 comments

·October 31, 2025

halo

On a similar note, I’ve always liked the idea of being able to mark functions as pure (for some reasonable definition of pure).

The principle of reducing state changes and side-effects feels a good one.

slifin

Yeah I wish variables were immutable by default and everything was an expression

Oh well continues day job as a Clojure programmer that is actively threatened by an obnoxious python take over

hyperhello

> I wish it was the default, and mutable was a keyword.

I wish the IDE would simply provide a small clue, visible but graphically unobtrusive, that it was mutated.

In fact, I end up wishing this about almost every language feature that passes my mind. For example, I don't need to choose whether I can or can't append to a list; just make it unappendable if you can prove I don't append. I don't care if it's a map, list, set, listOf, array, vector, arrayOf, Array.of(), etc unless it's going to get in my way because I have ten CPU cores and I'll optimize the loop when I need to.

throwaway2037

In my IntelliJ (a recent version), if I write a small Java function like this:

    private static void blah()
    {
        final int abc = 3;
        for (int def = 7; def < 20; ++def)
        {
            System.out.print(def);
        }
    }
The variable 'def' is underlined. Mouse-over hint shows: 'Reassigned local variable'. To be clear, 'abc' is not underlined. When I write Java, I try to use the smallest variable scopes possible with as much final (keyword) as possible. It helps me to write more maintainable code, that is easier to read.

NathanaelRea

I don't think this is the best option, there could be very hard bugs or performance cliffs. I think I'd rather have an explicit opt-in, rather than the abstraction changing underneath me. Have my IDE scream at me and make me consider if I really need the opt-in, or if I should restructure.

Although I do agree with the sentiment of choosing a construct and having it optimize if it can. Reminds me of a Rich Hickey talk about sets being unordered and lists being ordered, where if you want to specify a bag of non-duplicate unordered items you should always use a set to convey the meaning.

It's interesting that small hash sets are slower than small arrays, so it would be cool if the compiler could notice size or access patterns and optimize in those scenarios.

bbminner

Right, sql optimizers are a good example - in theory it should "just know" what is the optimal way of doing things, but because these decisions are made at runtime based on query analysis, small changes to logic might cause huge changes in performance.

bee_rider

I don’t have any useful ideas here but if you make a linter for this sort of thing, I suggest calling it “mutalator.”

nielsbot

I use Swift for work. The compiler tell you this. If a mutable variable is never mutated it suggests making it non-mutable. And vice versa.

bartvk

Yup, it's pretty great. You get into the habit of suspiciously eyeing every variable that's not a constant.

qmmmur

As will Typescript, at least using Biome to lint it does.

estimator7292

Your IDE probably supports this as an explicit action. JetBrains has a feature that can find all reads and writes to a variable

Denvercoder9

It also has the ability to style mutated variables differently.

greenicon

Yes, depending on your highlighting scheme. Not every highlighting scheme shows this by default, unfortunately.

To me, this seems initially like some very minor thing, but I find this very helpful working with non-trivial code. For larger methods you can directly discern whether a not-as-immutable-declared variable behaves immutable nonetheless.

worthless-trash

If you write in erlang, emacs does this by default ;)

HDThoreaun

Clang-tidy's misc-const-correctness warns for this. Hook it up to claude code and it'll const all non mutated mutables.

gwbas1c

Years ago I did a project where we followed a lot of strict immutability for thread safety reasons. (Immutable objects can be read safely from multiple threads.)

It made the code easier to read because it was easier to track down what could change and what couldn't. I'm now a huge fan of the concept.

piker

You should check out Rust

swiftcoder

Rediscovering one of the many great decisions that Erlang made

DarkNova6

To be fair, Carmack has advocated for immutability and other good practices at least since the 2000s.

rurban

Maybe he read SICP then. But he still didn't write a compiler since

signa11

erlang predates that.

eps

I doubt the GP tried to imply the opposite.

gorgoiler

I still have a habit of naming variables foo0, foo1, foo2, from a time working in Erlang many years ago.

lopatin

How fast this got to the top, you would think John Carmack just invented nuclear fusion.

gethly

Variable is by definition mutable.

Constant is by definition immutable.

Why can't people get it through their heads in 2025? (I'm looking at you, Rust)

munchler

> Making almost every variable const at initialization is good practice. I wish it was the default, and mutable was a keyword.

It's funny how functional programming is slowly becoming the best practice for modern code (pure functions, no side-effects), yet functional programming languages are still considered fringe tech for some reason.

If you want a language where const is the default and mutable is a keyword, try F# for starters. I switched and never looked back.

jandrewrogers

Pure functional works great until it doesn't. For a lot of systems-y and performance-oriented code you need the escape hatches or you'll be in for a lot of pain and annoyance.

As a practical observation, I think it was easier to close this gap by adding substantial functional capabilities to imperative languages than the other way around. Historically, functional language communities were much more precious about the purity of their functional-ness than imperative languages were about their imperative-ness.

christophilus

F# isn’t purely functional, though, and strikes a nice balance. I just don’t really like the .NET ecosystem, being 100% Linux these days, as it always seems slightly off to me somehow.

Fire-Dragon-DoL

Unfortunately unless there's an explicit way to state what has side effects like a mut keyword, a lot of fp programming advantages lose value because most devs default to mutable stuff, so the fp benefits don't compound

black_knight

I think this works quite well with IO in Haskell. Most of my code is pure, but the parts which are really not, say OpenGL code, is all marked as such with IO in their type signatures.

Also, the STM monad is the most carefree way of dealing with concurrency I have found.

jancsika

Hehe, purity is one helluva drug!

For some reason, this makes me think of SVG's foreignObject tag that gives a well-defined way to add elements into an SVG document from an arbitrary XML namespace. Display a customer invoice in there, or maybe a Wayland protocol. The sky's the limit!

On the other hand, HTML had so many loose SVG tags scattered around the web that browsers made a special case in the parser to cover them without needing a namespace.

And we all know how that played out.

Posted from an xhtml foreignObject on my SVGphone

josephg

> If you want a language where const is the default and mutable is a keyword, try F# for starters. I switched and never looked back.

Rust is also like this (let x = 5; / let mut x = 5;).

Or you can also use javascript, typescript and zig like this. Just default to declaring variables with const instead of let / var.

Or swift, which has let (const) vs var (mutable).

FP got there first, but you don't need to use F# to have variables default to constants. Just use almost any language newer than C++.

eitland

In Java you can use final[1]. And yes, if final points to an ArrayList you can change it, but you can also use final together with immutable data structures[2].

[1]: https://www.baeldung.com/java-final

[2]: https://www.baeldung.com/java-immutable-list

haspok

Did you know that "final" does not actually mean final in Java (as in: the variable can be constant folded)? Reasons include reflection and serialization (the feature that nowadays nobody uses, but due to backwards compatibility the Java language developers always have to worry about?). There was an excellent talk about this recently, I think triggered by a new JEP "stable values": https://youtu.be/FLXaRJaWlu4

Fire-Dragon-DoL

In typescript and js you get immutable references, but the data is mutable. Definitely not the same thing

bathtub365

Are there languages that automatically extend this to things like data structure members? One of the things I like about the C++ const keyword is that if you declare an instance of a struct/class as const it extends that to its members. If the instance isn’t const, you can still mutate them (as long as they aren’t declared const within the structure itself)

orlp

Rust works this way, yes. There are escape hatches though, which allow interior mutability.

umanwizard

If I understand what you’re asking correctly, rust is also like this. If you have a non-mut value of (or non-mut reference to) an object, you only get non-mut access to its members.

keeda

We are still living with the hangover of C, which was designed for the resource-starved machines of eons ago, and whose style later languages felt they had to copy to get any kind of adoption. (And as you point out, that is how things turned out.)

My bet is functional programming will become more and more prevalent as people figure out how to get AI-assisted coding to work reliably. For the very reasons you stated, functional principles make the code modular and easy to reason about, which works very well for LLMs.

However, precisely because functional programming languages are less popular and hence under-represented in the training data, AI might not work well with them and they will probably continue to remain fringe.

tasn

Functional programming languages (almost always?) come with the baggage of foreign looking syntax. Additionally, imperative is easier in some situations, so having that escape hatch is great.

I think that's why we're seeing a lot of what you're describing. E.g. with Rust you end up writing mostly functional code with a bit of imperative mixed in.

Additional, most software is not pure (human input, disk, network, etc), so a pure first approach ends up being weird for many people.

At least based on my experience.

kragen

Rust is not very suitable for functional programming because it is aggressively non-garbage-collected. Any time Rustaceans want to do the kind of immutable DAG thing that gives functional languages so much power, they seem to end up either taking the huge performance and concurrency hit of fine-grained reference counting, or they just stick all their nodes in a big array.

tayo42

Using a big array has good performance though?

rapind

> come with the baggage of foreign looking syntax

Maybe they're right about the syntax too though? :)

kragen

Which one, Erlang, Lisp, or ML?

rao-v

Exactly this! I’d love a modern C++ like syntax with the expressiveness of python and a mostly functional approach.

C# is not that far I suppose from what I want

tjk

Everybody's mileage will vary, but I find contemporary C# to be an impressively well rounded language and ecosystem. It's wonderfully boring, in the most positive sense of the word.

null

[deleted]

brrrrrm

one thing I've learned in my career is that escape hatches are one of the most important things in tools made for building other stuff.

dropping down into the familiar or the simple or the dumb is so innately necessary in the building process. many things meant to be "pure" tend to also be restrictive in that regard.

runevault

Functional languages are not necessarily pure though. Actually outside Haskell don't most functional first languages include escape hatches? F# is the one I have the most experience with and it certainly does.

Terr_

While it's amusing, I think it's sensible: One of the main tasks in most businessy programming is to take what a human wants, translate it to code, reverse-translate it back to human understanding later, modify it, and translate it again to slightly different code.

This creates friction between casual stakeholder models of a mutable world, versus the the assumptions an immutable/mostly-pure language imposes. When the customer describes what they need, that might be pretty close to a plain loop with a variable that increments sometimes and which can terminate early. In contrast, it maps less-cleanly to a pure-functional world, if I'm lucky there will at least be a reduce-while utility function, so I don't need to make all my own recursive plumbing.

So immutability and pure-functions are like locking down a design or optimizing--it's not great if you're doing it prematurely. I think that's why starting with mutable stuff and then selectively immutable'ing things is popular.

Come to think of it, something similar can be said about weak/strong typing. However the impact of having too-strong typing seems easier to handle with refactoring tools, versus the problem of being too-functional.

johncolanduoni

I’ve done a significant amount of functional programming (including F#) and still reach for it sometimes, but I don’t think it provides substantial advantages for most use-cases. Local mutability is often clearer and more maintainable.

Also, category theorists think how excited people get about using the word monad but then most don’t learn any other similar patterns (except maybe functors) is super cringe. And I agree.

the__alchemist

I think it's because (I'm looking at Haskell in particular) there are a lot of great ideas implemented in them, but the purity makes writing practical or performant time-domain programs high friction. But you don't need both purity and the various tools they provide. You can use the tools without the pure-functions model.

In particular: My brain, my computing hardware, and my problems I solve with computers all feel like a better match for time-domain-focused programming.

cogman10

My problem with functional languages is there never seems to be any easy way to start using them.

Haskell is a great example here. Last time I tried to learn it, going on the IRC channel or looking up books it was nothing but a flood of "Oh, don't do that, that's not a good way to do things." It seemed like nothing was really settled and everything was just a little broken.

I mean, Haskell has like what, 2, 3, 4? Major build systems and package repositories? It's a quagmire.

Lisp is also a huge train wreck that way. One does not simply "learn lisp" There's like 20+ different lisp like languages.

The one other thing I'd say is a problem that, especially for typed functional languages, they simply have too many capabilities and features which makes it hard to understand the whole language or how to fit it together. That isn't helped by the fact that some programmers love programming the type system rather than the language itself. Like, cool, my `SuperType` type alias can augment an integer or a record and knows how to add the string `two` to `one` to produce `three` but it's also an impossible to grok program crammed into 800 characters on one line.

v9v

> Lisp is also a huge train wreck that way. [...] There's like 20+ different lisp like languages.

Lisp is not a language, but a descriptor for a family of languages. Most Lisps are not functional in the modern sense either.

Similarly, there are functional C-like languages, but not all C-likes are functional, and "learn c-likes" is vague the same way "learn lisp" is.

mr_mitm

Is there a ruff rule for this?

noduerme

Why loops specifically? Why not conditionals?

A lot of code needs to assemble a result set based on if/then or switch statements. Maybe you could add those in each step of a chain of inline functions, but what if you need to skip some of that logic in certain cases? It's often much more readable to start off with a null result and put your (relatively functional) code inside if/then blocks to clearly show different logic for different cases.

turtletontine

There’s no mutating happening here, for example:

  if cond:
      X = “yes”
  else:
      X = “no”
X is only ever assigned once, it’s actually still purely functional. And in Rust or Lisp or other expression languages, you can do stuff like this:

  let X = if cond { “yes” } else { “no” };

That’s a lot nicer than a trinary operator!

nielsbot

Swift does let you declare an immutable variable without assigning a value to it immediately. As long as you assign a value to that variable once and only once on every code path before the variable is read:

    let x: Int
    if cond {
        x = 1
    } else { 
        x = 2
    }

    // read x here

ruszki

Same with Java and final variables, which should be the default as Carmack said. It’s even a compile time error if you miss an assignment on a path.

stevage

In JavaScript, I really like const and have adopted this approach. There are some annoying situations where it doesn't work though, to do with scoping. Particularly:

- if (x) { const y = true } else { const y = false } // y doesn't exist after the block - try { const x = foo } catch (e) { } // x doesn't exist after the try block

NathanaelRea

You could do an absolutely disgusting IIFE if you need the curly brace spice in your life, instead of a typical JS ternary.

  const y = (() => {
    if (x) {
      return true;
    } else {
      return false;
  })();

cookiengineer

Technically you could just use an assignment ternary expression for this:

    const y = (x === true) ? true : false;
I used this kind of style for argument initialization when I was writing JS code, right at the top of my function bodies, due to ES not being able to specify real nullable default values. (and I'm setting apart why I think undefined as a value is pointless legacy).

    Composite.prototype.SetPosition(x, y, z) {

        x = (isNumber(x) && x >= 0 && x <= 1337) ? x : null;
        y = (isNumber(y) && y >= 0 && y <= 1337) ? y : null;
        z = isNumber(z) ? z : null;

        if x !== null && y !== null && z !== null {
            // use clamped values
        }

    }

NathanaelRea

I typically only use ternaries for single operations and extract to a function if it's too big. Although they are quite fun in JSX. For your code i'd probably do:

  function SetPosition(x, y, z) {
    if (!(isNumber(x) && isNumber(y) && isNumber(z))) {
      // Default vals
      return;
    }
    x = clamp(x, 0, 1337);
    y = clamp(y, 0, 1337);
    z = z;
  }

fuzzythinker

nitpick: cleaner w/o ()'s, as '=' is the 2nd lowest operator, after the comma separation operator.

stevage

I love it.

askmrsinh

Why not do:

const y = x ? true : false;

stevage

I'm talking about cases with additional logic that's too long for a ternary.

keeda

Ditto. These days those are the only cases where I use "let" in JS. The thing I miss most from Kotlin is the ability to return values from blocks, e.g.

val result = if (condition) { val x = foo() y = bar(x) y + k // return of last expression is return value of block } else { baz() }

Or:

val q = try { a / b } catch (e: ArithmeticException) { println("Division by zero!") 0 // Returns 0 if an exception occurs }

Edit: ugh, can't get the formatting to work /facepalm.