Let variables shadow same name variables in the same block!

As a very new newbie to Rust I found myself confused by some code I was playing with. Turned out it had two variables, of different types, with the same name in the same block.

I can boil it down to an example like so:

pub fn main() {
    struct Thing {
        a: u32,
        b: u32
    };

    let x: u32 = 1;
    println!("x:{}", x);

    let x = 1.1;
    println!("x:{}", x);

    let x: Thing = Thing {a: 10, b: 11};
    println!("x: {},{}", x.a, x.b);

    let _y: u32 = 300;
    let (x, _y) = (100, 200);
    println!("x:{}", x);

    let x = (300, 400);
    println!("x:{:?}", x);
}

Now, I can understand a name shadowing the same name in a nested scope. But in the same scope it seems very wrong.

I have no idea if this is a bug or expected behavior.

The Rust Reference says this about "let":

"Any variables introduced by a variable declaration are visible from the point of declaration until the end of the enclosing block scope."

And it says this about the "Identifier patterns" used in let statements:

"The variable will shadow any variables of the same name in scope."

These seem contradictory to me. If it is shadowed it is not visible !

This is intended behavior. Among other things, it permits declaring an object as mut, building or modifying the object, and then redeclaring it without the mut to make it read-only for the duration of the block. Were this not supported, the redeclared object would need to have a different name and the original object would remain in-scope and mutable, thus violating Rust's guarantee of unique access to mutable objects.

4 Likes

I would not say this is true. It's still visible, you just can't name it. Code generated by a macro could still name it, however:

let x = 3;
macro_rules! get_x {
    () => (x);
}
let x = 4;

assert_eq!(4, x);
assert_eq!(3, get_x!());

Naively, it would appear that those last two statements expand to:

assert_eq!(4, x);
assert_eq!(3, x);

But in reality, those two identical looking identifiers x are in fact "colored" by different spans. The one generated by the macro has a span inside the macro definition, which is before the second x's definition; thus, it refers to the first x.

6 Likes

This example should go in The Book as a demonstration that Rust's hygienic macros are different than the textual substitution macros found in other languages (e.g., C).

3 Likes

Even with different names, move semantics will prevent access to the old name, unless it's a Copy type. There's no problem with mutable aliasing whether shadowing or not.

2 Likes

Ah, well, I had not got as far as macros in my Rust studies so far, but I suspected the same is true when capturing with a lambda, which indeed it is:

let x = 3;

let print_x = || {
    println!("x:{}", x);
};

let x = 4;
println!("x:{}", x);      // => x:4
print_x();                // => x:3

Which means if you happen to be looking at the last 3 lines there you can be mightily confused. Not having notice that other shadowed x above.

I see no useful purpose for such shadowing in the same scope but do see downsides. So why is it allowed?

Either way the language reference seem contradictory on this.

I think rust is fairly different from other languages with regards to shadowing. In my experience it’s a positive and idiomatic choice to repeatedly bind the same name in the same scope when the value being bound represents the same logical concept, but in different stages of processing. Converting a string to an int is a good example. In other languages, this may be more confusing, but rust’s strong typing and lifetimes make errors of confusion very unlikely in those cases.

In fact, it can be a benefit to rebind the name so other code can’t use the earlier value mistakenly, but the shadowed value can still be a part (via moves or references) of the newly bound one.

I like it, but it’s definitely something different to get used to.

4 Likes

It's certainly different.

I'm just not sure what the advantage of it is. Given the potential for confusion, which I experienced myself.

It's not clear to me that when you change a string to an int by shadowing that you can be thinking about the same logical concept.

I'm all in favor Rust's strong typing. That and Rust's other syntactic and semantic safety features is why I'm here after all. Which is why I find being able to change the type of a variable in the same block a very strange thing to be allowed to do.

Perhaps I'm missing a point here.

I believe they're talking about this sort of pattern, which is somewhat popular in Rust. (clearly opinions differ here, but the strong type system helps avoid mistakes.)

fn main() {
    let mut num: String = " 12345 ".to_string();
    
    num.retain(|c| c.is_ascii_digit());
    
    let num: u32 = num.parse().unwrap();
    
    println!("num: {}", num);
}

on the playground

5 Likes

Don't think of it like so. Think of it as declaring a new variable with a different name which just so happens to be named the same.

If you're loosing track of what type each variable has, then either your function is too long or you've got a problem in your logic.

Absolutely. It isn't often that I want to rebind a variable, but when I do it's because I want to make it clear to the reader that the old one is no longer accessible (without requiring then to read through the rest of the block).

My advice is simply to wait until you have much more experience with Rust and have read a lot of Rust code. Many people use shadowing to convert from an input value that has not been validated to a value that meets the Rust type constraints that have been declared for it, such as in @uberjay's example up-thread.

For example, a string received as input from an OS or non-Rust ABI is not known to be valid UTF-8. Many Rust coders would run that string through a sanitizer and rename the output to the same name as the input, converting from CStr to utf8. This is perfectly safe because rustc's strong type checking will catch every incompatible-type misuse.

2 Likes

OK. I'm warming to the idea with those examples. Thanks all.

2 Likes

In practice, the only time I've ever personally noticed any impact from this shadowing rule is when I'm doing "experimental" coding, where I copy and paste several lines then make a tiny change, and it "just works" without having to repeatedly comment out all the other blocks or change all the variable names just to make the copy legal. In other languages, I often end up wasting a lot of time "debugging" something that turns out to just be one copy accidentally using a variable name from the previous copy.

let x = 42;
println!("{:?}", x);

// blindly copy paste the above and add a &
let x = &42;
println!("{:?}", x);
// and both blocks compile fine

And it's never caused me any problems. So IMO it is a win, albeit a pretty small one.