Top
Best
New

Posted by azhenley 4 days ago

John Carmack on mutable variables(twitter.com)
508 points | 617 commentspage 5
zahlman 3 days ago|
> and it avoids problems where you move a block of code and it silently uses a version of the variable that wasn’t what it originally had.

I find that keeping functions short also helps a ton with that.

No, shorter than that. Short enough that the only meaningful place to "move a block of code" is into another function. Often, by itself.

williamdclt 3 days ago|
It helps with that, but it has other trade-offs: indirection isn't free for readability.
throw10920 3 days ago|||
I'll go further than that and say that indirection significantly increases cognitive load and hurts readability.
zahlman 3 days ago||
I have consistently found the opposite to be the case across decades of programming experience, as regards the extraction of helper functions. This is not "indirection" in the same sense as with data structures. It is abstraction of currently-irrelevant detail.

Reading and understanding code is a process of answering "what are the immediate steps of this task?", without thinking about what those steps consist of or entail. It is not a process of answering "where is the variable representing ..., and the code that manipulates this?", especially since this makes assumptions that may prove incorrect.

tengbretson 3 days ago|||
> indirection isn't free for readability

Yes, but also no. If its a mostly side-effect free function with a good name and well defined input/output types its basically free.

sgarland 3 days ago||
Dumb question from a primarily Python programmer who mostly writes (sometimes lengthy) scripts: if you have a function doing multiple API calls - say, to different AWS endpoints with boto3 - would you be expected to have a different variable for each response? Or do you delete the variable after it’s handled, so the next one is “new?”
ForHackernews 3 days ago||
If they're representing different data from different API calls, yeah, I'd be strongly inclined to give them different names.

    order_data = boto.get_from_dynamodb()
    customer_data = boto.get_from_rds()
    branding_assets = boto.get_from_s3()
    return render_for_user(order_data, customer_data, branding_assets, ...)
TZubiri 3 days ago|||
I think renaming an old variable is a common and sensible way to free a resource in python. If there are no valid names for a resource it will be garbage collected. Which is different in languages like C++ with manual memory management.

John Carmack is a C++ programmer apparently that still has a lot to learn in python.

sswatson 3 days ago|||
In the vast majority of cases, developer ergonomics are much more important than freeing memory a little earlier. In other scenarios, e.g., when dealing with large data frames, the memory management argument carries more weight. Though even then there are usually better patterns, like method chaining.

FYI John Carmack is a true legend in the field. Despite his not being a lifelong Python guy, I can assure you he is speaking from a thorough knowledge of the arguments for and against.

TZubiri 3 days ago||
>developer ergonomics are much more important than freeing memory a little earlier

Preach to the python choir bro, but it should be telling when a python bro considers it's too ergonomic and wasteful.

At some point being clean and efficient about the code is actually ergonomic, no one wants to write sloppy code that overallocates, doesn't free, and does useless work. To quote Steve Jobs, even if no one sees the inside part of a cabinet, the carpenter would know, and that's enough.

tl;dr: Craftmanship is as important as ergonomics.

sswatson 3 days ago||
In this case, overuse of re-assigning is the sloppy thing to do, and immutability by default is the craftsman's move. Reducing your program's memory footprint by re-assigning variables all the time is a false economy.
TZubiri 2 days ago||
So if you are preparing a 50Kb webpage, and you do 10 steps of processing, you would have a 500KB memory footprint that might be held during the life of the connection? All the while the footprint and thus capacity of your server could have been 100Kb? Nice craftmanship dude!

We are not even talking about in-place algorithms, just 10 functions that process an html into a new array, maybe:

html = load_template(route) html = formatstring(html,variables) html= localize_paths(html) ...

And you would rather have it:

template = load_template(route) formatted_html = formatstring(template,variables) html_with_localized_paths = localize_paths(html)

And you would rather have the latter? For what gain? I think you wouldn't.

"Only a sith deals in absolutes", you have to recognize that both are valid under different contexts. And I'm merely explaining why inmutable is the default in python, 1: python doesn't do programmer self restrictions like const and private; 2: memory is automatic, so there's no explicit allocation and freeing like in C++, so using a new variable for each thing isn't a zero overhead abstraction.

Even for smaller cases, (not 50kb arrays), it's still the proper thing to do, although you have freedom to choose, it's easier to just follow one style guide and protocol about how to do things, if it's pythonic to reuse the variable name, just reuse the variable name. Don't fall for the meme of coming from another language and writing C++ in python or Java in python, you are not a cute visionary that is going to import greatness into the new language, it's much better to actually learn the language rather than be stubborn.

There's places where you can be super explicit and name things, if it's just an integer then it's a very cheap comment that's paid in runtime memory instead of in LOC. But this is why the default in python is not const, because variable name reuse is a core python tactic, and you are not our saviour if you don't get that, you are just super green into the language.

maleldil 3 days ago||||
Not "a resource", but memory specifically. If there's a proper resource (e.g. a file), you should ensure it's explicitly released instead of relying on the GC (using with/close/etc.) And if memory usage is really important, you should probably explicitly delete the variable.

Anything else is wishful thinking, trying to rely on the GC for deterministic behaviour.

blueside 3 days ago||||
if this guys learns enough, who knows, he may have a future in programming!
ForHackernews 3 days ago|||
But wouldn't you do that inside a function or a loop body?
busfahrer 3 days ago||
In TFT, he mentions

> [...] outside of true iterative calculations

0xedd 3 days ago||
[dead]
AaronAPU 3 days ago||
This would require coming up with an order of magnitude more variable names which is just unnecessary cognitive load.
furyofantares 3 days ago||
An order of magnitude? That sounds like pretty outrageous hyperbole. A variable getting reassigned 10 times sounds extremely rare, the average in my experience has to be less than 1 reassignment. I think the approach requires coming up with maybe 10% more names.

Usually there are good, obvious names for intermediate calculations in my experience.

I'm open though - what kinds of things are you doing that require reassigning variables so much?

AaronAPU 3 days ago|||
Probably exaggerated a bit with that phrasing (“outrageous” seems similarly hyperbolic ;))

But any variable which I’ve not already marked as const is pretty much by definition going to be modified at least once. So now instead of 1 variable name you need at least two.

So now the average number of variables per non-const variable is >= 2 and will be much more if you’re doing for example DSP related code or other math heavy code.

You can avoid it with long expressions but that in principle is going against the “name every permutation” intention anyway.

furyofantares 3 days ago||
Fair enough re: "outrageous"!

It's actually math heavy code (or maybe medium heavy?) where I really like naming every intermediate. fov, tan_fov, half_tan_fov, center_x, norm_x

AaronAPU 3 days ago||
I spent a decade or so working on video codecs with an international team, and there was sort of an unwritten rule that code shouldn’t have comments and variable names shouldn’t be descriptive (english language couldn’t be assumed).

Which sounds really awful, but after a while it forces you to parse the logic itself instead of being guided by possibly-out-of-date comments and variable names.

I now prefer less verbosity so that probably explains why I’m a little out of distribution on this topic.

If you looked at any of my code prior to that job, it was the polar opposite with very “pretty” code and lengthy comments everywhere.

1718627440 2 days ago||
This is kind of the opposite of LLMs, they seem to derive meaning mostly from the variable names, not from what the code actually does.
AaronAPU 2 days ago||
I haven’t noticed that, will have to keep an eye out. Could help explain some quality inconsistencies.
chongli 3 days ago|||
No, not at all. You still have the advantages of scopes, name shadowing, namespaces, and collection types. If your language supports them, you can also use algebraic data types to further reduce the number of names you need to deal with.
jstimpfle 3 days ago||
Either you don't understand what you're talking about, or you've missed the word "strive" in the tweet.
AtNightWeCode 3 days ago||
I care less and less about things like this. At some point you will write code in some lang that have fancy keywords and stuff gets mutated anyway. Also, what people tends to do if stuff is immutable is that they hide mutation by doing deep copies with changes.
carabiner 3 days ago||
Does anyone have any real naming conventions, patterns for doing this in ds programming in notebooks? I've got a bad habit of doing:

  df = pd.read_excel()
  df = df.drop_duplicates.blahblah_other_chained_functions()
  [20 cells later]
  df = df.even_more_fns()
stevage 4 days ago||
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

latexr 3 days ago||
JavaScript’s `const` has the bigger issue that while things can’t be reassigned, they can still mutate. For example:

  const myArray = [1,2,3]
  myArray.push(4)
  myArray // [1, 2, 3, 4]
stevage 3 days ago||
In what way is that an issue?
maleldil 3 days ago||
Because this isn't immutability. The goal is to have a way to define an object that will never change after initialisation, and JS's const isn't it.
stevage 3 days ago||
Clearly that isn't the goal.
maleldil 3 days ago||
By "the goal", I mean TFA's, not JS's.
NathanaelRea 4 days ago|||
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 4 days ago|||
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 4 days ago|||
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;
  }
cookiengineer 4 days ago||
I always call this the difference of return branch styles. Yours I'd describe as "fast fail" aka return false as quickly as possible (for lack of a better terminology) whereas I personally prefer to have a single return false case at the bottom of my function body, and the other validation errors (e.g. in Go) are usually in the else blocks.

In JS, errors are pretty painful due to try/catch, that's why I would probably these days recommend to use Effect [1] or similar libraries to have a failsafe workflow with error cases.

Errors in general are pretty painful in all languages in my opinion. The only language where I thought "oh this might be nice" was Koka, where it's designed around Effect Types and Handlers [2]

[1] https://effect.website/

[2] https://koka-lang.github.io/koka/doc/index.html

fuzzythinker 4 days ago|||
nitpick: cleaner w/o ()'s, as '=' is the 2nd lowest operator, after the comma separation operator.
cookiengineer 1 day ago||
I guess I'm that one guy that likes expression brackets and statement ending symbols.
stevage 4 days ago|||
I love it.
askmrsinh 4 days ago|||
Why not do:

const y = x ? true : false;

stevage 4 days ago|||
I'm talking about cases with additional logic that's too long for a ternary.
1718627440 3 days ago|||
That sounds like a more complicated way to write

    const y = (bool)x;
or

    const bool y = x;
keeda 4 days ago||
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.

moi2388 2 days ago||
I agree. I mainly write c# nowadays, but even there I try to make everything as immutable as possible.

Makes everything so much easier to reason about.

mcv 3 days ago||
I use Javscript mostly. Or Typescript actually, these days. I remember when ES2015 introduced `let` because `var` had weird scoping issues. But ever since, I barely use either of them. Everything is `const` these days, as it should.
andsoitis 3 days ago||
> I use Javscript mostly. Or Typescript actually, these days. I remember when ES2015 introduced `let` because `var` had weird scoping issues. But ever since, I barely use either of them. Everything is `const` these days, as it should. reply

const prevents reassignment of the variable but it does not make the object the variable points to immutable.

To do the latter, you have to use Object.freeze (prevent modification of an object’s properties, but it is shallow only so for nested objects you need to recurse) and Object.seal (prevent adding or removing properties, but not changing them).

May people use immutable.js or Immer for ergonomic immutable data structures.

mcv 3 days ago||
That is an excellent point, and indeed a problem when debugging. When I log objects to the console, often they don't get serialized until I actually click on them, which means I don't get to see the object as it was at the time, but after a bunch of later changes.
LogicHound 3 days ago|||
`var` doesn't have weird scoping issues, it just different than other languages. `var` is function scoped, thus all var declarations are hoisted to the top of the function during execution.

This is why the single var pattern used to be recommended.

rlander 3 days ago||
Except const is not sufficient. It will prevent the reference from being reassigned but the const can still reference a mutable object.
wodenokoto 3 days ago||
Mutability was by far the most difficult thing when learning Python and mutating objects by iterating over its items do get confusing, even as a senior.

When I was first learning I thought all methods would mutate. It has a certain logic to it

Havoc 3 days ago|
Good point. Had never occurred to me that keeping steps help debug. Obvious in hindsight
Sammi 3 days ago|
For the same reason almost all my functions end with this:

  const result = ... ;
  return result;
I know debuggers can show return values, but that doesn't help when you're just doing a quick console log inspection.
More comments...