Top
Best
New

Posted by azhenley 3 days ago

John Carmack on mutable variables(twitter.com)
507 points | 617 commentspage 4
Const-me 3 days ago|
For performance critical code, you want to reuse L1D cache lines as much as possible. In many cases, allocation of a new immutable object boils down to malloc(). Newly allocated memory is unlikely to be found on L1D cache. OTOH, replacing data in recently accessed memory and reusing the memory is very likely to become L1D cache hit in runtime.
marginalia_nu 3 days ago||
For performance critical code, you wouldn't use malloc()-allocation at all, though whether using an arena allocator or putting stuff on the stack, your argument is still sane. Data locality is speed.
ramses0 3 days ago||
"Immutability" from a programming language statement perspective doesn't necessarily imply that the implementation duplicates memory or variables.

Similar to how "tail recursion can (usually) be lifted/lowered to a simple loop...", immutability from language statements can often be "collapsed" into mutating a single variable, and there may be one or two "dances" you need to do to either add helper functions, structure your code _slightly_ differently to get there, but it's similar to any kind of performance sensitive code.

Example foo(bar(baz(), bar_alt(baz_alt(), etc...))) [where function call nesting is "representing" an immutability graph] ...yeah, that'd have a lot of allocations and whatever.

But: foo().bar().bar_alt().baz().baz_alt().etc(...) you could imagine is always just stacking/mutating the same variable[s] "in place".

...don't get hung up on the syntax (it's wildly wrong), but imagine the concept. If all the functions "in the chain" are pure (no globals, no modifications), then they can be analyzed and reduced. Refer back to the "Why SSA?" article from a week or two ago: https://news.ycombinator.com/item?id=45674568 ...and you'll see how the logical lines of statements don't necessarily correspond to the physical movement of memory and registers.

Const-me 3 days ago||
You’re describing an edge case. Generally speaking, memory is only reused after old objects are deallocated. And here’s the relevant quote from the OP’s post:

> Having all the intermediate calculations still available is helpful in the debugger

garrison 1 day ago||
This reminds me of an earlier blog post by the same author: https://web.archive.org/web/20121111234839/http://www.altdev...
nyrp 3 days ago||
Is he referring to something specific with "true" iterative calculations vs. plain old iterative ones, assuming they are in some way "non-true" or less "true"? Like, avoiding i+=x in favor of ++i or something? Or maybe am I just tired today.
ufo 3 days ago|
I think he's just saying that mutation is ok if it's something loopy, like changing the loop counter or updating some running sum. So both i+=1 and ++i are fine.
thefaux 3 days ago||
Even better is to use tail calls instead of loops and eliminate mutable variables entirely.
rezonant 3 days ago||
https://xcancel.com/id_aa_carmack/status/1983593511703474196
seattle_spring 3 days ago|
CMV: HN should just automatically replace x links with xcancel
QuadrupleA 3 days ago||
Love Carmack, but hard disagree on this and a lot of similar functional programming dogma. I find this type of thing very helpful:

    classList = ['highlighted', 'primary']
    if discount:
        classList.append('on-sale')
    classList = ' '.join(classList)
And not having to think about e.g. `const` vs `let` frees up needless cognitive load, which is why I think python (rightly) chose to not make it an option.
ColeShepherd 3 days ago||
Some potential alternatives to consider:

1.

    classList = ['highlighted', 'primary']
        .concatif(discount, 'on-sale')
        .join(' ')
2.

    classList = ' '.join(['highlighted', 'primary'] + (['on-sale'] if discount else []))
3.

    mut classList = ['highlighted', 'primary']
    if discount:
        classList.append('on-sale')
    classList = ' '.join(classList)

    freeze classList

4.

    def get_class_list(discount):
        mut classList = ['highlighted', 'primary']
        if discount:
            classList.append('on-sale')
        classList = ' '.join(classList)
        return classList

    classList = get_class_list(discount)
salutis 3 days ago||
Fennel (Lisp):

    (table.concat [:highlighted :primary (if discount :on-sale)] " ")
warmwaffles 3 days ago||
This shouldn't be a hard and fast rule for everything. Be treated as guidelines and allow the programmer some wiggle room to reuse variables in situations that make sense.
AnotherGoodName 3 days ago|
Going too hard on mutation means you usually end up with larger structures that are recreated completely. Those themselves are then the point of mutation. This can be helpful if the larger object needs a lot of validation and internal cross rules (eg. You can't set 'A' if 'B' is true, you can validate that when recreating the larger object whereas if 'A' was mutable on it's own someone might set it and cause the issue much later which will be a pain to track down).

Anyway the outcome of trying to avoid mutation means instead of simply setting player.score you get something like player = new Player(oldPlayerState, updates). This is of course slow as hell. You're recreating the entire player object to update a single variable. While it does technically only mutate a single object rather than everything individually it's not really beneficial in such a case.

Unless you have an object with a lot of internal rules across each variable (the can't be 'A' if 'B' example above) it's probably wrong to push the mutation up the stack like that. The simple fact is a complex computer program will need to mutate something at some point (it's literally not a turing machine if it can't) so when avoiding mutation you're really just pushing the mutation into a higher level data object. "Avoid mutations of data that has dependencies" is probably the correct rule to apply. Dependencies need to be bundled and this is why it makes sense not to allow 'A' in the above example to be mutated individually but instead force the programmer to update A and B together.

considerdevs 3 days ago||
This is the kind of wisdom that comes after hours of debugging and discovering the bug was your own variable reuse.

Wouldn't this be an easy task for SCA tool e.g. Pylint? It has atleast warning against variable redefinition: https://pylint.pycqa.org/en/latest/user_guide/messages/refac...

maleldil 3 days ago|
This is only for redefinitions that change the type. If you re-assign with the same type, there's no warning. However, pylint does issue warnings for other interesting cases, such as redefining function arguments and variables from an outer scope.
noduerme 3 days ago||
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 3 days ago|
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 3 days ago|||
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 3 days ago|||
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.
sambishop 3 days ago|||
that's oldschool swift. the new hotness would be

  let x = if cond { 1 } else { 2 }
lock1 3 days ago|||
IMO, both ternary operator form & Rust/Haskell/Zig syntax works pretty well. Both if expression syntax can be easily composed and read left-to-right, unlike Python's `<true-branch> if <cond> else <false-branch>`.
koolba 3 days ago|
This is the mental distinction of what something represents vs what is its value. The former should never change regardless of mutability (for any sane program…), and the latter would never change for a const declaration.

The value (pun intended) of the latter is that once you’ve arrived at a concrete result, you do not have to think about it again.

You’re not defining a “variable”, you’re naming an intermediate result.

More comments...