top | item 35324307

(no title)

jackosdev | 2 years ago

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.

discuss

order

tialaramex|2 years ago

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.

MrJohz|2 years ago

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.

mr_00ff00|2 years ago

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.

masklinn|2 years ago

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.

eptcyka|2 years ago

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

alpaca128|2 years ago

> 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.

masklinn|2 years ago

> 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).

the_mitsuhiko|2 years ago

> 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

stouset|2 years ago

> 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 = …)

sophiabits|2 years ago

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.

cillian64|2 years ago

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.

jackosdev|2 years ago

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

iudqnolq|2 years ago

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

signaru|2 years ago

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.

FpUser|2 years ago

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

To me either has pros and cons.

ithkuil|2 years ago

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.

layer8|2 years ago

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.

pcwalton|2 years ago

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.

andrepd|2 years ago

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

tialaramex|2 years ago

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

alpaca128|2 years ago

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.