Why do I need to use mut when the variable is never reassigned?

Viewed 632

I thought I understood when and why I need to use mut in Rust code, but I don't understand why I need to use mut in the following code.

use rand::{thread_rng, Rng};

fn main() {
    let mut rng = thread_rng(); // Why does this need to be mutable?
    println!("{:?}", rng); // outputs ThreadRng { rng: UnsafeCell }

    let x: u32 = rng.gen();
    println!("{}", x);

    println!("{:?}", rng); // outputs the same result as ThreadRng { rng: UnsafeCell }
}

Let me explain. The variable rng is declared mutable but its value never changes throughout the code, so why do I need to make rng variable mutable?

2 Answers

There are 2 cases when you need to declare variable as mutable:

  1. When you change it value by assignment (you likely to already understand this):
let mut my_var = 5i32;
println!("old value {}", my_var);
my_var = my_var * 2;
println!("old value {}", my_var);
  1. When you get mutable reference to it:
let mut my_var = 5i32;
println!("old value {}", my_var);
let reference = &mut my_var;
*reference = 10; // Mutable reference allow us change the value
println!("old value {}", my_var);

In code above would be quite surprising because if we declare my_var as let my_var because it's actual value changes.

Your case is exactly it. You call this method: https://docs.rs/rand/0.8.3/rand/trait.Rng.html#method.gen Your code can be rephrased as:

let x: u32 = Rng::gen(&mut rng);

As you see, you take a mutable reference here by calling a method which uses &mut self.

I suspect this question arises from the difference in the concept of "variable" in Rust from that in managed languages like Python or Java. In Python, there is an implicit indirection between variable and the object it holds, so:

n = 42
n += 1         # the variable changed, but not the value (here immutable)

# however:
l = []
l.append(42)   # the value changed, but not the variable
l = [1, 2, 3]  # the variable changed, but not the value

In the second part, the variable l refers to a list object that exists independently of the variable. That object contains both the list elements and the metainformation such as list size. l.append(42) mutates the list object, while l = [1, 2, 3] mutates the variable to refer to a different (freshly created) list object. Multiple variables can easily refer to the same object, which can be a source of bugs.

Languages like Rust and C++ don't work like that. There is no implicit indirection between a variable and the value it contains, variables store the actual value. (Indirections can still exist, but they are either explicit using Box and Rc or hidden in the implementation of the object, as is the case with Vec, but are never provided by the language, they always have to be explicitly requested.) And two variables cannot contain the same object, because the object is the variable. For example:

let mut n = 42;
n += 1;             // the contents of the variable changed

// but also:
let mut l = vec![];
l.push(42);         // the contents of the variable changed
l = vec![1, 2, 3];  // the contents of the variable changed

In other words, the data that comprises the vector (the triple of <ptr, len, capacity>) is stored in-line in the variable, so every mutation of the vector counts as a modification of the variable. For example, when you append an element to the vector, the vector's length will change, so a piece of data stored directly in l will actually get modified. The only difference between invoking a mut method on l and assigning to l is that in the case of assignment, all fields are likely to change at once. But there is no fundamental difference between the two like there is in Python - both have the effect of modifying the contents stored in the variable. Since variables don't refer to independently-existing objects, the two types of mutation are indistinguishable as far as the language is concerned.

The same applies to the rng variable: it holds an RNG instance whose state must change when you generate the next number. This state change counts as change to the variable, just as does assigning it a brand new RNG. A value in Rust doesn't have inherent "identity" which would distinguish the assignment from other kinds of mutation to the state.

If you wanted to emulate the Python semantics, you could use a smart pointer:

// doesn't need to be mutable, holds reference to heap-allocated RNG
let rng = Rc::new(RefCell::new(thread_rng()));

// RefCell::borrow_mut() grants permission to mutate the shared object,
// but `rng` (the Rc pointer) is still unchanged
let x: u32 = rng.borrow_mut().gen();

// as expected, this wouldn't compile because rng is not mut:
//rng = Rc::new(RefCell::new(thread_rng()));
Related