A few questions on shadowing and the let keyword

I am on day two of my Rust endeavor. I am seeing some similarities with Python in shadowing. Does shadowing then, work like object references or does entering and exiting scope break mutability? Either of these two reasons satisfies my thinking on why the mut keyword is unnecessary. Hopefully that is a self-contained question. I saw some explanations regarding into, but that's too advanced for me right now (it does seem to resemble args and kwargs with unpacking in Python also).

Secondly, with let I am seeing it seems like the Javascript keyword because the initial statement I got about it from a book was that by default (in an example using let) variables are immutable. Yet I see things like this are allowed:

let x = "three";
let x = 10;

Does this have something to do with the variation of the type?

Thanks!

1 Like

In this case, there are two different bindings named x. After creating the binding of x pointing to the value 10, the previous x str reference is no longer accessible.

This inability to access the previous binding (shadowing) is primarily useful when you're changing the type but the human meaning of the value is the same.

2 Likes

So then there are two ways to use shadowing, one being to access scope, another being explicit changes in type as you said. Ok.

For reference, in the Udemy course I'm following in conjunction with "Rust by Example", the instructor uses this:

let t = 10;
let t = t + 10;
v = 30;
{
     v = 40
     println!("v is {}", v)
}

In the first example I took the reassignment of t using t as shadowing which is one instance in which an object reference would be created in Python and the original object reference would be removed. I suppose on a second look discussing the scoped variables is redundant as it actually has nothing to do with shadowing. Using scope in this way, simple with:

{
     x = 40;
}

is likewise new to me.

Python has very little concept of declaring variables; a variable is created by its first assignment. This is very much not how Rust works.

You should think about shadowing as if the variables are renamed (internally to the compilation process, in a way you can't access). That is, the code

let t = 10;
let t = t + 10;

is precisely equivalent, in how it behaves when run, to

let t_1 = 10;
let t_2 = t_1 + 10;

Two variables. Not one. No reassignment (as would happen in Python) is occurring.

I suppose on a second look discussing the scoped variables is redundant as it actually has nothing to do with shadowing.

Well, all variables have a scope. A variable's scope is the textual region in which the variable can be accessed (if not shadowed by another of the same name). But a variable's scope starts where it is declared and not before, hence shadowing can be done within a single {...} block, without necessarily also creating one.

4 Likes

Binding mutability (let mut ...) is a property of bindings (variables) and not scope. Scope can change what binding is "visible", though.

fn visible() {
    let mut x = 10;
    {
        // Inner `x` shadows outer `x` (for now)
        let x = 42;
        println!("inner x: {x}");
        // This is an error as the "visible" `x` is not a `mut` binding
        // x += 10;
    }
    // Outer `x` becomes "visible" again
    x += 10;
    println!("outer x: {x}");
}

I'm not entirely sure this is what you meant, but it's true that mut bindings aren't crucial to Rust's safety guarantees and could, in principle, be removed. mut bindings can be considered a lint which prevents you from overwriting the variable or taking a &mut _ to the variable. But you can create a new binding to change binding mutability. (It's also somewhat of a misnomer as it doesn't prevent mutation in the face of interior mutability (aka shared mutability), for example.)

&mut _ and &_, on the other hand, are distinct types with crucial differences.

This is true, but note that the original variable is still around (like the visible example showed). It will go out of scope at the same place and may also still be accessible in some way via other variables, like references.

fn not_nameable_but_still_around() {
    let mut s = "hello".to_string();
    let r = &mut s;
    let s = vec![()];
    r.push_str(", world!");
    println!("{r}");
}

I don't have the full context, but regardless, it seems like a weird example. Putting the two statements into an inner block is pointless.

Incidentally, Rust By Example is somewhat out of date and could use some other corrections. I did a review recently,[1] which you can find here. The notes go follow the order of RBE (when I wrote it anyway). The portions about bindings are

And here's a playground with the two examples from above.


  1. which will hopefully evolve into PRs if I can find the time ↩︎

2 Likes

No. It has nothing to do with the type. It's all about scopes. Even if the type of the second variable is exactly the same as that of the first one, it's still another, distinct variable.

Python doesn't have shadowing, it only has assignment. Rust's shadowing is nothing like a re-assignment in Python. A shadowing variable does exactly nothing to the shadowed (original) one. It doesn't change its value. It doesn't change its type. It doesn't change its scope.

Shadowing is a merely syntactic, compile-time device that causes the same name to refer to a new, completely independent variable.

Shadowing has nothing to do with "object references" (assuming you mean indirection) or mutability, either.

3 Likes

I find this language needlessly confusing.

You can't change the type of a variable, you also can't change its mutability. A different variable can have a different type and different mutability, but that doesn't change the original variable's type or mutability.

Shadowing is like two people randomly having the same exact name. They are still separate variables.

Another note because this may be confusing: "binding" is Rust lingo for "variable". The two words mean the same exact thing.

I also find this language needlessly confusing. "lints" are Rust lingo for "warnings". Warnings differ from errors in that they can be ignored / disabled. Attempting to mutate a non-mut variable is a hard error, not a warning, and it can't be ignored.

This is actually correct, if a little confusing. Missing mut is, of course, an error. But one could use mut everywhere - and this would be nothing more than a warning with no changes to the semantics.

2 Likes

Okay, let me rephrase, hopefully for the better.


mut bindings like let mut, or the lack of mut in a binding, don't indicate some intrinsic property of the value. For example it's not part of the value's type, and you can move the value to a new binding:

let s = "Hello".to_string();
let mut s = s;
s.push_str(", world");
let s = s;

I guess I could say "it is like a lint" or "similar to a lint" instead of "considered a lint". The point is that all bindings can be declared mut without changing the semantics (or soundness) of a program. I was addressing the OP's comment about the keyword being unnecessary. In principle we don't need non-mut bindings.

But yes, you can't disable the need to declare some bindings mut and that will probably always be true.

It's not a property of the value at all. Values are never mutable. The value "Hello" is always "Hello", just like the value 42 is always 42. Mutability is a property of a binding (of a variable), not of a value.

Let's remove shadowing confusion from your example:

let s1 = "Hello".to_string();
let mut s2 = s1;
s2.push_str(", world");
let s3 = s2;

s1 is immutable and its value never changes -- it's always "Hello".

s2 is mutable and its value switches from "Hello" to a different value, "Hello, world". Mutable variables are characterized by being able to contain different values at different times.

s3 is immutable and its value never changes -- it's always "Hello, world".

Mutability of each of the three variables is always maintained and never changes. You can also call it an "intrinsic property" of each of the three variables. s1 and s3 are always immutable, s2 is always mutable.

Semantics. "Underlying data" if you prefer.[1]

Lots of introductory material present a dichotomy between "let mut is mutable data, let [no mut] is immutable data". But as both my and your examples show, you can move a String from a non-mut binding to a mut binding and mutate the underlying data. That's the potential misunderstanding I'm addressing.

We're agreed on that. Namely, that's the ability to overwrite a variable or take a &mut _ to the variable.


  1. The values of s1/s2/s3 aren't the str data on the heap either, if you want to be this pedantic. ↩︎

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.