Values, Names, and the “let” Word
mut on purpose.
Naming a value
You give a value a name with let:
fn main() {
let age = 30;
println!("age is {age}");
}
▶ Run this in the Rust Playground
Reading: “let age be 30”. The arrow only goes one way. age is bound to 30. The binding is the important word.
Now try to change it:
fn main() {
let age = 30;
age = 31; // won't compile
println!("age is {age}");
}
Rust will refuse to compile this. The error is short and clear:
error[E0384]: cannot assign twice to immutable variable `age`
Why “immutable by default”
let vs let mut
Every language eventually learns the same lesson: the bugs that are hardest to find are the ones where a value changed somewhere you were not looking. Rust’s answer is “fine, then values do not change unless you explicitly say so.” You opt in with mut:
fn main() {
let mut age = 30;
age = 31;
println!("age is {age}");
}
▶ Run this in the Rust Playground
In JavaScript const and let, it's the exception that something should not change. In Rust, it's the rule. You write mut only when you actually need to change the value, which turns out to be less often than you think.
Types are usually inferred
Notice we did not write a type for age. Rust looked at 30 and decided age is an i32 — a 32-bit signed integer. You can spell it out when you want:
#![allow(unused)]
fn main() {
let age: u8 = 30; // 0..=255
let price: f64 = 9.99; // a double-precision float
let name: &str = "Ada"; // borrowed string slice
let ok: bool = true;
}
The types you will see in the first month:
- Integers:
i8 i16 i32 i64 i128(signed),u8 u16 u32 u64 u128(unsigned). Default:i32. - Floats:
f32,f64. Default:f64. - Booleans:
bool. - Characters:
char— one Unicode scalar, written'a'. - Strings:
&str(a borrowed view of text) andString(an owned, growable string). We will make this distinction real in Chapter 5.
Shadowing: a reused name, not a changed value
This compiles, and it is a common Rust pattern:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("{spaces}");
}
▶ Run this in the Rust Playground
spaces was a string. Now, by a second let, it is a number. The old binding is shadowed by the new one. No value was mutated — a new binding just took the name.
Imagine a whiteboard label. let mut is a label on a slot: you can replace what is in the slot. Shadowing is the label itself moving to a new slot. The old slot is still there, you just stopped caring about it.
Watch what the compiler does with each of these bindings — including the one it refuses.
Interactive simulation (requires JavaScript): assigning to an immutable binding fails with E0384 before the program ever runs; declaring the binding with mut allows the stack slot to be updated in place.
- Make a
let mut counter = 0;, then increment it three times, then print it. - Make an immutable
let pi: f64 = 3.14;and try to change it. Read the error carefully. - Use shadowing to change
let age = "30";into a number, usinglet age: u32 = age.parse().unwrap();Run it.
Next: the four shapes every Rust program uses to organize data — struct, enum, tuple, array.