Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I really like in Rust how you can reinit a variable with a different type e.g. “let rect: Rect<f32> = rect.into();”

It’s just so damn useful and I’m not sure what the downside is, it sucks when you have to keep coming up with different names so you can keep around an identifier that you don’t need anymore.



This is idiomatic Rust, it works very nicely there, however most languages aren't Rust

Rust's Into::into() is consuming the object in the old (now shadowed) rect variable. So conveniently the old rect variable which we can't access also no longer has a value†. In many languages a method can't consume the object like that, so the old object still exists but we can't access it because it is shadowed.

For example in C++ they have move semantics, but their move isn't destructive, so the object is typically hollowed out, but still exists until the end of the scope at least.

Rust's type strictness matters here too. It means if you later modify some code using rect meaning whatever it was before that statement morphing it into a Rect<f32> chances are it doesn't type check and is rejected. For example in many languages if (rect) { ... } would be legal code and might change meaning as a result of the transformation, but in Rust only booleans are true or false.

† Unless this previous variable's type implemented the Copy trait and therefore it has Copy semantics and consuming it doesn't do anything.


This is all true in this instance, but it doesn't have to be at all. You could write something like the following:

    let name = "Arthur".to_owned();
    //... Do something with &Arthur

    let name = "Bethan".to_owned();
    //... Do something with &Bethan
In this situation, ownership of the first string was never passed on, and the value will be dropped (deallocated, destructed, etc) at the end of the scope, meaning that also in Rust, the first string value is shadowed and becomes inaccessible, much like in your description of C++. In addition, because in this case both variables have the same type, you can use second variable thinking that you're using the first one, and the compiler will not help you, you'll just end up using the wrong name somewhere.

Fwiw, I find this feature very useful, and it's helpful more often than it is a nuisance. But there are no guarantees that you're consuming or transforming the object you're shadowing, and the compiler won't necessarily help you out if you simply accidentally use the same name twice.


This is interesting to me that C++ allows you to access a value after move is called. Presumably it wouldn’t be hard for the compiler to yell at you.

I assume accessing it is undefined behavior?

I would assume you could change this without affecting backwards compatibility.


As another commenter says the moved-from object should have "Valid but unspecified state" (types provided by the standard library will do that, custom types merely should do that)

Since you don't know what valid state it has, calls with pre-requisites are nonsense (e.g. if you have a Bird and the method land requires that the Bird should be flying, you can't call land() on a moved from Bird, because you don't know if it's flying) but all calls without pre-requisites are fine e.g. asking how long a string you moved is would work - it's probably zero length now, but maybe not.

> Presumably it wouldn’t be hard for the compiler to yell at you.

In the general case this is Undecidable, so, the opposite of not hard.

> I would assume you could change this without affecting backwards compatibility.

C++ which relies on this exists today, the most likely path to actually landing destructive move in C++ would be to add a whole new set of construction and assignment operators for destructive move, forcing people to opt in and adding to the many sets C++ already has, and likely angering C++ developers a great deal in the process.

Howard Hinnant, whose design today's non-destructive move is, did argue that in principle it's possible to add destructive move to the language later if desired, but his description rather undersells the benefits of this design, presumably because he couldn't deliver it. Maybe he'd watched enough Mad Men (yup, Mad Men's early seasons pre-date C++ having move semantics) to know you shouldn't tell the customer what they can't have or they'll want it.

Common things to actually do with a C++ variable after moving from it are:

* Nothing, but in the knowledge it won't be cleaned up until the scope ends * Re-assign it, destroying the hollowed out object immediately * Re-use the hollowed out object, e.g. call a clear() method on it and then use as normal


The only requirement placed on the “moved out” variable is that you should be able to call its destructor. Which means that it has to be in a valid but unspecified state. So it's fine to access such a variable, so long as you don't read its exact state. You can still assign to it, for instance.


Definitely super useful, especially in a language where such conversions are rather common.

Also useful because you can’t have abstracted local types, so let’s say you’re building an iterator in a language with interfaces you could do something like

    let it: Iterator = some().thing();
    // intermediate stuff
    it = it.some().transform();
    // more intermediate stuff
    it = it.final().transform();
But in Rust that won’t work, every adapter yields a different concrete type, you’d have to box every layer to make them compatible. Intra-scope shadowing solves that issue.

The biggest downside is that it’s possible to reuse names for completely unrelated purposes, which can make code much harder to understand. Clippy has a shadow_unrelated lint but it’s allowed by default because it’s a bit limited.


You could just create new bindings for each new `it`, `let it = ...; let it = it.too();`


That’s the point, you can because rust supports intra-scope shadowing.

If it didn’t you’d have to type-erase, or create a new independently-named binding for every step, as you do in e.g. Erlang (can’t say this is / was my favourite feature of the langage).


Yes, the fact that "V = expression" means "if variable V doesn't exist, assign expression's value to it; otherwise compare the expression's value with the value of V and raise exception if they're not equal" is one of my least favourite parts of Erlang.

I semi-regularly introduce local variables named exactly like one of the function's parameter and then spend several minutes trying to understand why the line expression on the right-hand side of assignment throws badmatch: of course, it doesn't, it's the assignment itself that throws it.


Yes that’s also an interesting facet of the language. IIRC it makes sense because of the Prolog ancestry, so it kinda-sorta looks like unification if you squint, but boy is it annoying.


> I’m not sure what the downside is

The downside is that you may get a weird bug and only after a while see that you accidentally overwrote a function parameter and the Rust compiler didn't even warn you about it.

For this reason I always add the following line to my projects to enable warnings:

    #![warn(clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated)]
You can also use "deny" instead of "warn" to make it an error. I also like "#![deny(unreachable_patterns)]", which detects bugs in enum pattern matching if you accidentally match "Foo" instead of "Type::Foo" - I honestly don't know why this isn't set by default.


> The downside is that you may get a weird bug and only after a while see that you accidentally overwrote a function parameter and the Rust compiler didn't even warn you about it.

If you “overwrite” a function parameter without using it, the compiler will warn you of an unused variable.

If you “overwrite” a function parameter because you’re converting it, it’s a major use case of the feature.

> I honestly don't know why this isn't set by default.

Because the author of the match can’t necessarily have that info e.g. if you match on `Result<A, B>` but `B` is an uninhabited type (e.g. Infallible), should the code fail to compile? That would make 95% of the Result API not work in those cases. Any enum manipulating generic types could face that issue.

IIRC it was originally a hard error, and was downgraded because there were several edge cases where compilation failed either on valid code, or on code which was not fixable (for reasons like the above).


> If you “overwrite” a function parameter because you’re converting it, it’s a major use case of the feature.

Or it's unintended and thus a bug. I personally almost never intentionally shadow variables so I turned it into warnings.

> e.g. if you match on `Result<A, B>` but `B` is an uninhabited type (e.g. Infallible), should the code fail to compile?

This specific example you chose is probably the least relevant here, as the Result type doesn't require you to write "Result::Err(_)" instead of just "Err(_)", both will correctly match. Which can of course also be done for custom enums by "importing" their variants ("use EnumName::*;"). But in my experience it's easy to accidentally omit the type in the match pattern and then suddenly it matches everything. I personally can't imagine a situation where this is intentional and have spent way too much time debugging this specific issue, hence I choose to turn it into an error.


> The downside is that you may get a weird bug and only after a while see that you accidentally overwrote a function parameter and the Rust compiler didn't even warn you about it.

It will absolutely warn about this:

    fn foo(i: u32) -> u32 {
        let i = 42;
        i
    }

    fn main() {
        dbg!(foo(42));
    }
results in

    warning: unused variable: `i`
     --> src/main.rs:1:8
      |
    1 | fn foo(i: u32) -> u32 {
      |        ^ help: if this is intentional, prefix it with an underscore: `_i`
      |
      = note: `#[warn(unused_variables)]` on by default


I don't know what the difference was, but 1-2 years ago it definitely did not warn me. Perhaps it doesn't show a warning when you assign a different datatype?


The unused variable warning has been around since before 1.0, and works even when the types of the variables are different.


There's only one case where shadowing has bitten me in the past: long methods with loops dealing usize almost exclusively, where shadowing external bindings inside the loop might make sense, but any mistake would be silent. This was in the context of terminal layout code. The solution there has been extensive testing, but what I should have done is split the megafunction into multiple smaller ones.


> you accidentally overwrote a function parameter

To "accidentally" overwrite it, you have to either:

  a) explicitly mark the parameter binding as mutable: fn foo(mut bar: T)
  b) explicitly re-bind the variable with let (let bar: T = …)


Exactly! I was thinking of shadowing in Rust when I wrote my original comment.

My day job is predominantly in Typescript and a lot of code winds up reading significantly worse than it needs to. A common pattern for me is unique-ifying some sort of array—“const dataUnique = new Set(data);” is horrible, and if there’s no reason to keep the original “data” variable in scope then it’s doubly bad; I want to keep as little context in my head as possible.


The downside is when reading code you’re keeping in your head information about the type of each variable. If you skim through the code and miss one of these redefinitions then you may be mistaken about the variable’s type.

That said, I still think sparing use of this is justified, especially with an editor which can show types on mouseover.


That’s true, but this has never been a problem for me looking through large codebases and doing code reviews, in other languages I was constantly annoyed by not being being able to shadow


You've got to keep info either way. I'm more worried about forgetting

    let data = get();
    let uniqueData = Array.from(new Set(data));
    // ... (snipped many lines)
    process(data); // should have been uniqueData


At the opposite end of the spectrum, there are languages with case insensitivity and even style insensitivity. I personally avoid them, but it's interesting how the users of these languages have a very different philosophy.


I use C++ and Delphi / Lazarus. I guess I have a "very different philosophy" then I ;)

To me either has pros and cons.


there are things that make perfect sense when a language forces you to use an IDE anyway if you want to do anything longer than a toy.

Shadowing is not a big deal with IDEs; you can always see the type of the variable , jump to definition easily etc etc.

The rule to not shadow variables makes more sense when you want to understand the code just by looking at it.


With shadowing, you can use or mutate a variable, thinking you are using/mutating the outer instance because you’re unaware of the inner (shadowing) instance, which is the one you are really using/mutating. An IDE doesn’t help catching such an inadvertent error (unless it warns about shadowing variables, but then you’d want to rename it anyway, to get rid of the warning).

I’ve tripped over unexpected shadowing often enough that I wish more languages would forbid it. I rarely have trouble choosing appropriate variable names to avoid shadowing.


it's a footgun indeed and no IDE per se doesn't solve all the problems. But since rust was mentioned, there are other rust features that make that less of a problem: most of the rust code uses immutable variables and only rarely you do use mut variables and mut references and these can be under bigger scrutiny by reviews and linters.

I focused on IDEs in my comment because I find shadowing to be a problem even with immutable variables, because it's hard for you to tell what is the type of a variable if it keeps change throughout the function body.


I fought to keep this feature around in Rust. I was inspired by OCaml (which the old Rust compiler was written in), where you could write:

    let x = foo() in
    let x = bar x in
    let x = baz x in
    print x
In a functional language where mutation is less convenient than in C++, this is really handy, and I wanted Rust to support the same idiom.


Well why not with the same type? Sounds like an arbitrary restriction: you can use this idiom, but only sometimes.


There is no such restriction, it's just much less common to want that.

  let x: u32 = 5;
  let x: u32 = 10; // You can write this, but why?
  let x: u32 = 20; // I really feel like you should re-consider
If you end up shadowing this way in a long function it more likely means the function got too long. On the other hand, I certainly have had cause to shadow variables in inner scopes e.g.

  let x = some_complicate_stuff();
  for dx in [-1, 0, 1] {
    let x = x + dx;
    // Do stuff with x very naturally here, rather than keep saying "x + dx" everywhere
  }
  // But outside the loop x is just x, it's not x + dx


Why would you do that with the same type instead of just making the variable mutable? And you can do it, I just don't think it's a good idea as you now effectively have a mutable variable without it being marked as such.


No, it's still better than a mutable variable. Because it's not a mutable variable, just a series of variables that happen to have the same name.

Mutable state is 'evil' and makes your program harder to reason about on a semantic level. Shadowing is merely a syntactic choice with pros and cons.

I like shadowing in Rust, it works well there. In eg Python or Haskell, it works less well, but for different reasons. (In Haskell it's because of laziness and definitions being co-recursive by default. In Python it's because the language doesn't give you any tools to tell apart assignment to an existing variable from creation of a new variable.)


> it's not a mutable variable, just a series of variables that happen to have the same name.

Fair point, though in that case I'd be more comfortable separating those variables into scopes.

> Mutable state is 'evil' and makes your program harder to reason about on a semantic level. Shadowing is merely a syntactic choice with pros and cons.

Both result in multiple states of the same identifier, so I don't quite see the big difference here. In Rust I already have the clearly visible "mut" keyword telling me that it'll be overwritten.


> Both result in multiple states of the same identifier, so I don't quite see the big difference here.

Shadowing is something you can figure out purely on the syntactic level.

Figuring out mutable state requires solving the halting problem.

> Fair point, though in that case I'd be more comfortable separating those variables into scopes.

Well, they effectively have different scopes. The scope is just not delimited with curly braces.

It's similar to how eg Haskell's variable binding in do-notation extent to the rest of the do-block. Each line introduces a new scope.

However, I can re-interpret your comment as saying that you want a more explicit syntactic marker for a new scope. And that's a fair enough request.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: