Rust Basics #2 Variables and Types: let and mut, Shadowing, Functions That End in Expressions
In Part 1 we saw that reassigning a variable declared with let is a compile error. This part starts from exactly that point. Variables, types, and functions are the basics, but Rust made different choices on all three — and those choices become the foundation for understanding ownership later.
let and mut: why immutable is the default #
let count = 1; // immutable — no reassignment
let mut total = 0; // mutable — reassignment allowed
total += count;In most languages, “variables change by default; only constants get special marking.” Rust is the reverse: not changing is the default, and only variables that change carry mut. When you read code, a variable without mut is guaranteed to keep its declared value to the end, so you only need to track the places where values can change — the amount of code you must read shrinks. The borrowing rules in Part 4 are also built on this immutable/mutable distinction, so mut is not a convenience marker but an axis that runs through the whole language.
const exists separately. It is a true constant fixed at compile time, requires a type annotation, and is written in uppercase by convention.
const MAX_RETRIES: u32 = 3;Shadowing: redeclaring the same name #
There is one way to appear to change a value without mut: redeclaring the same name with let, called shadowing.
let input = "42"; // type &str
let input: i32 = input.parse().unwrap(); // same name, now type i32
Unlike reassignment, this creates a new variable that hides the previous one, so even the type can change. The pattern above — receiving input as a string and parsing it into a number — is the classic case. Rather than multiplying names like input_str and input_num, keeping the same name is the Rust idiom. It also guarantees, at the level of names, that the pre-conversion value will not be used afterwards.
Scalar types: integers come in sizes #
| Category | Types | Notes |
|---|---|---|
| Integers | i8–i128, u8–u128, isize, usize | default inference is i32 |
| Floating point | f32, f64 | default inference is f64 |
| Boolean | bool | true, false |
| Character | char | a 4-byte Unicode scalar |
The integer list looks long, but the practical instinct is simple: i32 unless there is a reason otherwise, usize (a pointer-sized unsigned integer) for collection indexes and lengths, and u8 for byte data. The fact that char is not one byte but a 4-byte Unicode scalar will come back in Part 7, where we handle strings.
Overflow behavior is worth knowing. In debug builds, integer overflow panics immediately; in release builds it wraps around and keeps going. The design intent is to catch “silently becoming a weird value” during development. When wrapping or saturating is what you actually want, you say so with explicit methods like wrapping_add and saturating_add.
Type inference is strong, so annotations are often omitted, but where inference cannot decide (like the parse result above), an annotation is required.
Tuples and arrays: fixed-size bundles #
let point: (f64, f64) = (3.0, 4.0);
let x = point.0; // access by position
let (px, py) = point; // destructuring
let days: [u32; 3] = [31, 28, 31]; // the length is part of the type
let jan = days[0];A tuple is a fixed bundle of possibly different types; an array is a fixed-length bundle of one type. In both, the length is part of the type, so the size is settled at compile time. The variable-length collection you will use far more in practice, Vec, comes in Part 7 after ownership. If an array index goes out of bounds, Rust does not read the neighboring memory like C — it stops immediately with a panic. This is the same consistent attitude: loud failure over quiet misbehavior.
Functions: the last expression is the return value #
fn add(a: i32, b: i32) -> i32 {
a + b
}Parameter types and the return type (->) are always written out, never inferred — the signature is the documentation. The eye-catching part is that there is no return, and it is not a typo. In Rust, if the last expression of a block has no semicolon, its value becomes the value of the block. Add a semicolon after a + b and it becomes a statement that discards the value, producing a compile error along the lines of “expected i32, found ()” — with a help line telling you to remove the semicolon, so there is no need to panic.
if is an expression too, so it produces a value. That is why there is no separate ternary operator.
let price = if is_member { 800 } else { 1000 };return is used only for early exits in the middle of a function. This “almost everything is an expression” property shows its full value with match in Part 5.
Summary #
- Variables are immutable by default; only the ones that change carry
mut. It shrinks what a reader has to track, and it underlies the borrowing rules later. - Shadowing redeclares the same name with
letand can even change the type. It keeps parsing and conversion code free of name clutter. - The practical defaults:
i32for integers,usizefor indexes,u8for bytes. Overflow panics in debug and wraps in release. - The last expression of a block is its value. A semicolon separates statements from expressions, and
ifproduces values too. - The next part is the steep hill and the heart of this course: ownership — how memory gets cleaned up without a GC.