`mut` vs. redefinition

This is a code style question.

What do you prefer, redefining a variable:

let a = 1.0;
let a = -a;

Or defining a variable as mutable:

let mut a = 1.0;
a = -a;

And why?
I speak when there are not control flow blocks. No if, while, etc.. Obviously, when they exist, we have no choice but to use the second approach.
Edit: To clarify, I know it's possible even with control flow. But let's talk about the simple case.

I can see advantages to both styles. When this allows me to use compound operators, I strictly prefer the second. That is, I write:

let mut length = 1;
length += 1;

And not:

let length = 1;
let length = length + 1;

Because of the noise.

But when there are not compound operators, I have doubts what to choose. I can prefer the second because its conciseness, or the first because it reduces the risk for an unwanted mutation. After all, functional languages have only the first for this reason.

Edit: Also, I can see an advantage to the second version because it's easier to understand what variable we refer to in the declaration, esp. for programmers without much background in functional languages.

For non-trivial types there's a semantic difference: new definition doesn't do anything to the old value. It's not changing the old variable, it just creates a new one that happens to have the same name.

5 Likes

Both styles are readable so it really doesn't make much of a difference. I don't think there's a single hard and fast rule, but there are factors that can lead to me choosing once over the other.

For mut:

  • Compound operators are nice.
  • The variable is some expensive to clone type like a vec, mutating it is cheaper than allocating a new one.

For shadowing:

  • I only need to mutate the variable once, having an immutable type might be preferable.
  • The variable is some cheap to clone/copy type like an integer. If it's cheap to copy, it really doesn't matter if you mutate or shadow.
let a = 1.0;
let a = if rng.gen::<bool>() {
    2.0
} else {
    3.0
};

:slight_smile: You can do this with loops too by using break <some value>.

The way I read that is:

  1. Create an "a" for me, as some floating point type, and initialize it with 1.0.

  2. Create another "a" for me and initialize it to the value of that previous "a". Oh and by the way, throw that other "a" away.

All of which seems kind of daft. Likely the compiler will optimize that so the same storage is used for both "a"'s, but that is beside the point.

Where I do appreciate such shadowing is where I have some value, which deserves a meaningful name, but I want to change the type I am using to represent it:

let speed = read_speed();    // May well be a float or whatever number type. 
...
let speed = s.to_string();

Here, I have the same value with the same name, only the type has changed for convenience. This at least saves having to think up different names for the same thing, "speed_f32", "speed_str" or whatever.

4 Likes

Of course, when we want to change the type we still have no choice.

But overall, this is what I think too.

I'm not sure that not "having to think up different names" is worth the problems shadowing can cause. I had a bug that took forever to find along the lines of

fn foo(bar: Bar) {
      // A whole bunch of code that among other things defines qux
      let bar = qux;
     // A whole bunch of code 
     // Use the new bar when I thought I was using the function argument

Shadowing has it's uses, but I wish my IDE would let me know when I did it.

By "control flow" I meant for example:

let mut length = 1;
while length < 10 {
    length += 1;
}

Or:

let mut length = 1;
// Do some stuff with `length`
if f() {
    length = 2;
}

Of course, both can be modified to use redefinitions (this is how functional languages do that), but this feels too artificial to me.

With the names you use here, there's no surprise :laughing:

But to take it a little more seriously, I find shadowing one of the most useful features of Rust. Like I said, programmers that used to imperative languages can find it weird, but I also have a functional background, and for me Rust took the best of both worlds. Knowing to work with this feature is hard, I agree, but completely worth it.

I don't disagree that shadowing is useful. I'm just pointing out that it can be a source of bugs. A subtle indication in my IDE that I've redefined a variable would allow the benefit while giving me a fighting chance to find the kind of bug I managed to create.

Of course you're right, and it's important for newcomers here to note that. There's also difference in the compiler perspective, although performance-wise the both are same (I guess the former takes a little more time to optimize). But after we said that, there's still a place for this discussion, IMHO.

It's somewhat off-topic here, but I would like to say that like the joke in the beginning, it heavily depends on the names you use. IMO, good naming will make shadowing not threatening anymore.

I have oft' heard people say that.

Some how I have never run into such a problem.

Perhaps because when I do it the scope over which the shadowing occurs is only a handful of lines of code, so any such problem shows up pretty quickly.

But note: If you do something like that, like so:

let mut length = 1;
while some_condition {
    ....
    let length = length + whatever;
    ...
}

Then the "length" inside the loop is not the same "length" as outside the loop.

After the loop you have the original "length" and it's value "1" again.

I guess this is where such shadowing could cause confusion and bugs.

1 Like

@ZiCog You're right, and not just that, even in the next iteration the variable will reset. This is not the correct way to rewrite the loop in functional style. Ah, loops are forbidden in functional programming :smile:

Edit: See Rust Playground. I used for loop and not while because if I would rely on the length variable to end the loop it was never terminating :slightly_smiling_face:

I sometimes use both together with this kind of pattern:

let mut x:ComplicatedValue = Default::default();
/* Build `x` the way I want it */

let x = x;
/* Now `x` is immutable, so I know it won’t be accidentally changed. */
5 Likes

Certainly cool :smiley:

Although I think the difference is subtle and no real reason to strictly prefer either exist, I see that just like I though, there are programmers that prefer the mutable approach. So, to help future programmers... You know :slightly_smiling_face:

Just in case anyone is wondering, I use long, descriptive names. A typical function name is forward_to_out_channel and a variable name recv_port_no. I've had complaints from coworkers who prefer cryptic abbreviations, but, given my age, it's the only way I can remember what I did last week. In spite of that, I managed to inadvertently shadow a name.

1 Like

Hah! What was your longest name? I wanna compare :smile:

Edit: In my current open file, the longest name I can find is _interpolations_open_parens_count_mut(), where "parens" stands for "parenthesis" :smile:

Edit 2: Or generate_read_and_declaration_token!()? Let me compare...

The former is 2 letters longer :slight_smile:

A quick scan turned up send_next_item_or_empty. I'll do a more comprehensive search if someone will give me a regular expression I can use.

Regex which counts? I don't think this is possible. A little scripting maybe.

Edit: Anyway, a quick regex to Rust identifiers is /[_A-Za-z][_A-Za-z0-9]*/.

Edit 2: Mine is longer!!

Edit 3: Now I've found non_top_level_local_declaration(), skip_whitespaces_and_comments(). Stop time spending, back to work :smiley: