Binding, rebinding, mutable?


#1

This doesn’t make sense to me.

https://doc.rust-lang.org/book/variable-bindings.html

let x: i32 = 8;
let x = 42;

vs.
let mut x = 8;
x = 42

The different is that x remains mutable in the second,
but “remains” immutable in the first?

But if I can just say let on every assignment, then
nothing is immutable.

I think the first form should be an error.
Unless the previous binding is mutable.
Then it can be used to change type.

I appreciate let x = x lets me remove mutability,
but I think the ability to add mutability is dubious.
Adding mutability I think degrades immutability too much.

  • Jay

#2

As for whether this is good or bad, you can find a lot of past discussion of this if you search for “shadowing” in the various Rust forums. I think there are some reasonable arguments in both directions. It’s definitely suprising to newcomers, but I haven’t found it produces any real surprises once you’re used to Rust semantics. It also allows some useful idioms.

Some notes that I hope might clarify things…

First, shadowing is a bit different from arbitrary mutation, if you look beyond simple types like integers. For example, if x is an immutable binding to a struct:

let x = MyStruct::new(foo);
// ...

…then you can create a new x bound to a totally new struct:

let x = MyStruct::new(bar); // okay!

…but you can’t mutate x’s fields or borrow a mutable reference to it, unless you explicitly move it to a mutable binding:

x.name = "puppydog"; // ERROR
x.mutate(); // ERROR
let y = &mut x; // ERROR

Second, most other C-family languages also allow shadowing, but many restrict it to inner scopes shadowing outer scopes. For example, this is legal C or C++:

const int x = 0;
{
    const int x = 1;
}

You can think of let in Rust as always introducing a new scope. (This what let does in OCaml, an important influence on Rust’s syntax and semantics.) So this code:

let x = foo();
let y = bar();
// ...

is sugar for this:

let x = foo();
{
    let y = bar();
    // ...
}

This has several consequences, some of them subtle. One is that the lifetimes of these two variables are different; y is destroyed before x. (This matters in cases where you want to do something like borrow a reference to y and store it in x.) And in the case of shadowing, it means that this code:

{
    let mut x = File::open("foo.txt");
    let mut x = File::open("bar.txt");
}

is not equivalent to this code:

{
    let mut x = File::open("foo.txt");
    x = File::open("bar.txt");
}

If you run them under strace you’ll find they make a different sequences of system calls. The first one is equivalent to:

{
    let mut x = File::open("foo.txt");
    {
        let mut x = File::open("bar.txt");
        // both "foo.txt" and "bar.txt" are open here
    } // "bar.txt" is closed here
} // "foo.txt" is closed here

while the second results in this sequence of actions:

{
    let mut x = File::open("foo.txt");
    x = File::open("bar.txt"); // "foo.txt" is closed here
    // only "bar.txt" is open here
} // "bar.txt" is closed here

This difference is observable for any type with a destructor: Vec, Ref, MutexGuard, etc. It can also make a difference if there are live borrows of the shadowed value. With shadowing, the original value is still alive; it’s just shadowed. Without shadowing, the original value is dropped when the new one is assigned.


Inquiring about idiomatic usage of shadowing
#3

There is difference between the first and second. In your first, the original value 8 don’t be freed, still can be accessed, and rebinding is to make new address to store new value 42. In second, no new address declared, but change the data stored in the original address. For example:

    let x: i32 = 8; // bind x to value 8;
    println!("{:p}", &x);  // print address of value 8
    let a = &x; // borrow value 8, a can see as a pointer to address of value 8 
    let x = 42; // rebind x to value 42, means that make new address to store value 42, and value 8 still live  but can't accessed by x anymore 
    println!("{:p}", &x); // print address of value 42, different to address of value 8
    println!("{}", a); // access value 8 by a, that means value 8 still live
    println!("{:p}", a); // print address of value 8

    let mut y = 8; // bind y to value 8, mutable means data stored in address can be changed 
    println!("{:p}", &y); // print address of value 8, no new address declared
    y = 42; // change value stored in original address to value 42
    println!("{:p}", &y); // print address of value 42, same with origin

Result is:

0x7ffe4f35c5b4
0x7ffe4f35c51c
8
0x7ffe4f35c5b4
0x7ffe4f35c37c
0x7ffe4f35c37c


#4

Interesting. This actually does have value for the very language obsessed C/C++ programmer.

Is let an expression?

There is the following idiom in C/C++.

There are a few forms of it.
It isn’t necessarily about exceptions vs. error code, or Windows-specific, but one common form is:

 HRESULT hr; // error code 

 hr = f1(); 
 if (FAILED(hr)) 
  return hr; 

 hr = f2(); 
 if (FAILED(hr)) 
  return hr; 


 hr = f3(); 
 if (FAILED(hr)) 
  return hr; 

This is flawed.
You don’t want that initial uninitialized hr, nor do you want to initiliaze it arbitrarily.

You want kinda:

 HRESULT hr = f1(); 
 if (FAILED(hr)) 
  return hr; 

 hr = f2(); 
 if (FAILED(hr)) 
  return hr; 


 hr = f3(); 
 if (FAILED(hr)) 
  return hr; 

But this is oddly assymmetric – the call to f1 should look like the calls to f2/f3.
If I add more calls to the sequence, the edit should be more the same no matter if I add
before f1 or after.

So you want:

 HRESULT const hr = f1(); 
 if (FAILED(hr)) 
  return hr; 

 HRESULT const hr = f2();
 if (FAILED(hr)) 
  return hr; 

 HRESULT const hr = f3(); 
 if (FAILED(hr)) 
  return hr; 

But that isn’t legal in C/C++.

Rust?

 if (FAILED(let hr = f1())) 
  return hr; 

 if (FAILED(let hr = f2())) 
  {  // oh hey special handling
  printf("f2 failed %X\n", hr);  
  return hr;  
 } 

 if (FAILED(let hr = f1())) 
  return hr; 

Is that legal?
In this specific pattern you really want exceptions anyway, but
I’m sure this pattern occurs otherwise.

This could also fix for loop variable scope in C++.
In particular, you want this:

 for (int i = 0; i < n; ++i) 
  printf("%d\n", a[i]; 

 // no i in scope here 
 
 for (int i = 0; i < n; ++i) 
  printf("%d\n", b[i]; 
 
 for (int i = 0; i < n; ++i) 
  printf("%d\n", c[i];  

which is how it works these years.
It used to not be. It flip flopped.

But you really want an occasional:

  for (int i = 0; i < n; ++i)  
   printf("%d\n", a[i];  
 
  for (int i = 0; i < n; ++i)  
   // i ? printf("%d\n", b[i] : break;  // not legal because break isn't an expression -- possibly fixed in Rust. 
   if (i) printf("%d\n", b[i]);  
   else break; 


 // special handling  

 if (i != n) // not legal 
 printf("stopped b after %d iterations\n", i); 
  
  
 for (int i = 0; i < n; ++i)  
  printf("%d\n", c[i]);   

which I believe is not legal.

So in C++ you are forced to compromise and if you want i to outlive
the for loop, you have to declare one line earlier, and worry about
initializing it…

Thank you,

  • Jay

#5

No, it is a statement.


#6

Rust encourages the use of the Result type so that if the rust version of f1 looked like so:

fn f1() -> Result((), HRESULT) {
  //...
}

then you could use the nifty if-let syntax

if let Err(hr) = f1() {
  return hr;
}

#7

let isn’t an expression, but break, return, if, and match are, among other things. For the exact pattern you’re describing, there’s no especially succinct representation, but since Rust allows shadowing you can at least use a let each time without differentiating the first instance:

let hr = f1();
if failed(hr) { return hr; }
let hr = f2();
if failed(hr) { return hr; }

But that’s unidiomatic: to prevent mistakes, it’s preferred to separate different kinds of return values, such as error codes versus some other information returned on success, using an enum, rather than stuffing them into one integer type and differentiating based on sign or whatever. If you have a function that returns Ok(info) or Err(err_code), you can write:

let info = match some_func() {
    Ok(info) => info,
    Err(rc) => return Err(rc), // diverging expressions like 'return' can be used where any type is expected
};

which is… actually somewhat verbose, but it’s nice and structured (there’s nowhere that info is in scope without being initialized and actually representing a successful return), and since that pattern so common the try! macro exists to reduce it to let info = try!(some_func()). In more complex situations I find it handy to be able to use most control flow as an expression.


#8

if let is its own thing https://doc.rust-lang.org/book/if-let.html