Hacker News new | ask | show | jobs
by lolinder 498 days ago
> Replacing the value of one variable constantly throughout the code could lead to unpredictable bugs.

Having variables with scopes that last longer than they're actually used and with names that are overly long and verbose leads to unpredictable bugs, too, when people misuse the variables in the wrong context later.

When I have `initial_foo`, `foo_feature_a`, and `foo_feature_b`, I have to read the entire code carefully to be sure that I'm using the right `foo` variant in subsequent code. If I later need to drop Feature B, I have to modify subsequent usages to point back to `foo_feature_a`. Worse, if I need to add another step to the process—a Feature C—I have to find every subsequent use and replace it with a new `foo_feature_c`. And every time I'm modifying the code later, I have to constantly sanity check that I'm not letting autocomplete give me the wrong foo!

Shadowing allows me to correctly communicate that there is only one `foo` worth thinking about, it just evolves over time. It simulates mutability while retaining all the most important benefits of immutability, and in many cases that's exactly what you're actually modeling—one object that changes from line to line.

3 comments

> When I have `initial_foo`, `foo_feature_a`, and `foo_feature_b`, I have to read the entire code carefully to be sure that I'm using the right `foo` variant in subsequent code. If I later need to drop Feature B, I have to modify subsequent usages to point back to `foo_feature_a`. Worse, if I need to add another step to the process—a Feature C—I have to find every subsequent use and replace it with a new `foo_feature_c`. And every time I'm modifying the code later, I have to constantly sanity check that I'm not letting autocomplete give me the wrong foo!

When you have only one `foo` that is mutated throughout the code you are forced to organize the processes in your code (validation, business logic) based on the current state of that variable. If your variables have values which are logically assigned you're not bound by the current state of that variable. I think this a big pro. The only downside most people disagreeing with me are mentioning is related to ergonomics of it being more convenient.

> When you have only one `foo` that is mutated throughout the code you are forced to organize the processes in your code (validation, business logic) based on the current state of that variable. If your variables have values which are logically assigned you're not bound by the current state of that variable.

If I'm understanding you right, this is just restating what I said as a positive thing. I stand by my assertion that it's not positive: you can always choose to leave previous states accessible by choosing different names. But if a language doesn't support shadowing then I don't have the capability to intentionally restrict myself from accessing those states. That means your language has less expressive power and fewer opportunities for me to build myself guardrails.

In some ways it's the opposite of unused variable warnings: if you disallow shadowing, the compiler is forcing you to leave variables accessible long after you need them. You're given no choice but to leave unused variables hanging around. With shadowing, I can choose the right path based on the situation.

> The only downside most people disagreeing with me are mentioning is related to ergonomics of it being more convenient.

As I said elsewhere, literally everything to do with programming languages is about ergonomics. Your arguments against shadowing boil down to ergonomics. You can't avoid having a debate by just saying "it's just ergonomics" when the debate that you started is which feature is more ergonomic!

It’s a trade-off.

If you allow shadowing, then you rule out the possibility of the value being used later. This prevents accidental use (later on, in a location you didn't intend to use it) and helps readability by reducing the number of variables you must keep track of at once.

If you ban shadowing, then you rule out the possibility of the same name referring to different things in the same scope. This prevents accidental use (of the wrong value, because you were confused about which one the name referred to) and helps readability by making it easier to immediately tell what names refer to.

And on the whole, I prefer shadowing. I’ve never had a bug in either direction, but keeping everything immutable without shadowing means you spend all your brain power Naming Things.
I mean that's a really fake problem. How many times per line of code do you actually need to name variables and how many of those times you're shadowing a previously defined var. I'm guessing a very small amount.
It's not just the naming things, it's also what you do after you've named them—if you can't shadow a name then you are stuck both coming up with new names and sifting through all the existing names in your autocomplete to try to remember which one is the real one at this point in the code. Get it wrong? There's a bug.

That's not a fake problem, it's a problem I've actually run into on a regular basis on languages that don't have shadowing.

It absolutely depends on the language and how heavily it encourages immutability. For example, Rust and Elixor allow shadowing.

An awkward middle ground for me is Kotlin. It allows shadowing, but warns, so it might as well not be allowed. So you end up using lots of scoping tricks to avoid either making everything mutable, or having dozens of nearly-identical variables.

In case of rust, it actually happens quite often. I find myself rarely needing to use mut, instead using functional approaches such as iterators and expressions. So a high percentage of the code is let statements
Exactly. Shadowing is a super important complement to Rust's immutability. Without it immutability would be less useful and therefore less used.
I think it's worth pointing out that the example in the article contains a bug caused by not having shadowing: "const foo3 = try foo.addFeatureB();" should not be using the original foo, but foo2.
I don't know zig at all, but why is the author trying to declare foo as const 3 times. Surely you would declare it as var with some default value that means uninitialized, then try and put values in it.
It's probably a Zig antipattern, but it's a very common Rust pattern. Shadowing in Rust allows immutability to be ergonomic, and lack of shadowing discourages immutability.

Zig isn't Rust, so it makes sense that patterns in Rust don't translate well, but also I totally get TFA's preference for Rust in this case.

Oh, I just read the rust doc and its says "once a value is bound to a name, you can’t change that value." but I've thought of immutability the other way around, once a name has a value, it can't be changed.

I thought the value of const was once you read const x = 1024, you can be sure that x is 1024 while its in scope, that subsequent code can make assumptions about the content of variable x. Or, when you see x in the code, you can jump directly to its definition and know what its value will be. Defined once and not changed.

Apparently I don't understand the value of const at all.

There is a distinction between the variable itself and its name. Const (and Rust's immutability-by-default) ensures that the variable does not change after assignment. This holds true even as references to it are passed to other functions or stored for later use. You "can't" accidentally pass a reference to that variable which will then be unexpectedly mutated a dozen calls deep into a library function you didn't write.

If you have shadowing, it simply means you can have a different variable with the same name later in the same (or child) scope, this usually must be explicit. The same name now refers to a different variable, but the original variable still exists and remains valid.

It's quite a useful pattern, particularly where the old value is no longer useful (for example transforming input), especially when using the old value might be valid code but would be a mistake.