Rebinding variables with let


#1

Hello Rust community,

I am a total Rust noob, curious about the language and started reading the manual. My background is from dynamic programming languages but I have written in C, C++, D. When I saw let rebinding a variable symbol to a different type in the manual within the same scope I thought it had to be a mistake. Then I tried it on the compiler:

let mut x: i32 = 1;
x *= 3;
println!("Initial value of x: {}", x);
let x = "Hello World!";
println!("Next value x: {}", x);

it compiled and ran. I couldn’t believe it! I find this feature very intriguing and wonder how it works - is it a runtime or a compile time feature, and how is the variable x managed? Does this rebinding carry a performance cost?


#2

They are completely different variables.
So, the code is not logically different from this:

let mut x1: i32 = 1;
x1 *= 3;
println!("Initial value of x: {}", x1);
let x2 = "Hello World!";
println!("Next value x: {}", x2);

The only difference is that in your code, both variables share the same name.
The second is hiding the first one and you cannot use the first variable while the second is in scope.


#3

I see, so the scope of the first x is from when it is first declared to when the second let statement re-declares it. Can I also extrapolate from what you are saying that this variable scope resolution occurs at compile time and does not carry a run-time performance cost?


#4

Right, this is compile-time only feature - no runtime cost.


#5

That’s awesome.

Thanks


#6

If by scope you mean “when can I refer to this variable”, then yes, if “when this variable is alive”, then no.
Consider the following code:

let x = 5;
let x_reference = &x;
let x = "foo";
println!("{}", x_reference);

This code compiles! For the x_reference to be valid at the time of printing, the original x variable has to be actually alive even after the let x = "foo".

This behaviour is actually useful sometimes, consider this example (type annotations added for clarity):

let line: String = String::from("   foo  ");
let line: &str   = line.trim();
// line is now "foo"

In this case, the second line is a borrowed string – &str, and this borrowed string is actually part of the first line. If the first line would really stop existing, the borrow would be invalid.

This may be confusing at first, but if you don’t care about the previous binding anymore and you don’t want to invent new names for each variable, shadowing can be useful technique.


#7
let x_reference = &x;
let x = "foo";
println!("{}", x_reference);

That’s a feature that one has to be careful with, x_reference print resolves to 5 which depending on your view may be unexpected but it’s still useful.

Earlier I did play with:

let x = (1, "two", 3.0);
let x = (x, 'x');
println!("{}", (x.1));

which is the behaviour I am looking for. For me a possible use case is when you have a tuple type for instance a simple table made up of vectors - where each vector is a different type but it is internally atomic, you could bind columns to the table - which would create a new type and then reassign the table back to the original variable. (I am aware that my example is not the equivalent of column binding - but demonstrates modifying a variable type and reassigning it back to the same variable name). This way you don’t have to resort to sub-typing polymorphism - essentially having to type-hide each column type under an umbrella generic class so that you can have an array of the parent class, where the actual type of the child elements are hidden.

Of course I have only started touching the Rust language so I don’t know what other difficulties that there could be around implementing this till I actually decide to but I think variable shadowing is a very useful feature and can certainly think of other situations where I would use it!


#8

I’m not quite sure what you have in mind here, but the applications of name shadowing to type polymorphism are questionable at best!

I think you should find that static typing with name rebinding is equivalent in power to static typing without name rebinding; except that maybe you don’t need to invent names like

let unique_inputs = inputs.unique();
// blah
// blah
let good_inputs = unique_inputs.filter(|c| ...);
// blah
// blah
let inputs_that_wont_kill_us = good_inputs.filter(|c| ...);
// blah
// blah
let even_inputs = inputs_that_wont_kill_us.every(2);

quite nearly as frequently.

…and that, as a result, people are less afraid to design APIs that cause their users to unwittingly produce types like Every<Filter<Filter<Unique<<Self as MyCrazyThing<u64>>::Inputs, (closure at derp.rs:577)>, (closure at derp.rs:573)>>>>


#9

BTW, C supports this:

int x = 1;
{
char *x = "hi";
}

so you can think of Rust’s behavior same as C, except it auto-inserts {}.


#10

My example was looking at the application of name shadowing in use with tuples instead of sub-typing polymorphism. The advantage would be from the point of view of the programmer using the library. With a tuple type in other static compiled languages, you would have to do something like:

auto table2 = table1.ColumnBind(newColumnFloatVector1)
auto table3 = table2.ColumnBind(newColumnStringVector2)

Because adding or removing a column would change the type if the underlying data structure - which is a tuple. Each operation would require a new variable name, which is awkward when the user wants in-place modification of table columns.

The main alternative is to use sub-typing polymophism, you create an parent class called GenericVector and Vector types that are sub-types of GenericVector. Then you use an array of GenericVector instead of a tuple for your underlying data structure. In this case adding and removing columns doesn’t change the compile time type, it is still a GenericVector array … only now you have to create all the necessary machinery to deal with your new run-time types.

Another alternative is to use void pointers.

Remember, this is for a simple table structure, not a table for a database application.