R Rust by Evidence PDF

Chapter 1 — Values, Bindings, and Mutability

Goal

Understand the difference between a value, the name bound to it, and permission to replace that value.

Prediction

fn main() {
    let score = 10;
    score = 11;
    println!("{score}");
}

What happens?

A. It prints 11 because variables are mutable by default.
B. It prints 10 because assignment is ignored.
C. Compilation fails because score was not declared mutable.
D. Compilation fails because integers cannot change.

Reveal: read the evidence

Answer: C.

The useful part of rustc's report is:

error[E0384]: cannot assign twice to immutable variable `score`
  |
2 |     let score = 10;
  |         ----- first assignment to `score`
3 |     score = 11;
  |     ^^^^^^^^^^ cannot assign twice to immutable variable
help: consider making this binding mutable
  |
2 |     let mut score = 10;
  |         +++

The integer is not inherently frozen. The binding named score lacks permission to be assigned another value.

Build the model

Working model: let creates a name for a value. The name is immutable unless you write mut.

let mut score = 10;
score = 11;

This says more than “Rust likes extra keywords.” A reader can see which bindings are expected to change. Accidental reassignment elsewhere becomes a compiler error.

A binding is not the value itself. That distinction appears clearly with shadowing:

fn main() {
    let input = " 42 ";
    let input = input.trim();
    let input: u32 = input.parse().expect("a whole number");
    println!("{input}");
}

Each let input = ... creates a new binding. The old binding is hidden, or shadowed. Because this is a new binding, its type may change from &str to u32. Reassignment cannot do that:

let mut input = "42";
input = 42; // expected `&str`, found integer

Precise model: a binding associates a pattern with a value for some scope. Without mut, you cannot assign through that binding. Shadowing introduces a distinct binding, even when it reuses the same spelling. Mutability is also not deep or universal: later you will see that a mutable binding does not override borrowing rules.

Values whose size is known at compile time often live directly in the current stack frame, but do not use “binding equals stack slot” as a language rule. The compiler may optimize storage away. Ownership and permitted operations are the reliable model; physical addresses usually are not.

Simplest fix

Declare expected reassignment:

fn main() {
    let mut score = 10;
    score += 1;
    println!("{score}");
}

This prints 11. Use mut when one logical thing changes over time: a counter, accumulator, or buffer.

Tradeoff alternative: shadowing

fn main() {
    let score = 10;
    let score = score + 1;
    println!("{score}");
}

Shadowing keeps each binding immutable and permits a type change. It is useful for staged transformation. Its cost is that too many same-named stages can make debugging and error messages harder to follow. Do not shadow merely to avoid an honest mut.

Mini challenge

What prints?

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("{spaces}");
}

A. Three spaces
B. 3
C. It fails because spaces changes type.
D. It fails because spaces is immutable.

Answer

B. The second let creates a new usize binding. No immutable binding is reassigned. The program prints 3.