Build the mental model
Rust is statically typed, so every variable's type must be known at compile time — the compiler can often infer it from context (`let x = 5;` defaults to `i32`), but ambiguous contexts need an explicit annotation like `let x: u32 = 5;`. There are four scalar types: integers (`i8` through `i128` signed, `u8` through `u128` unsigned, each with a precisely fixed bit width), floating-point numbers (`f32`, `f64`), booleans (`bool`), and characters (`char`, a 4-byte Unicode scalar value). There are two compound types: tuples (fixed-length groups that can mix types, like `(i32, f64, char)`) and arrays (fixed-length, same-type elements, like `[i32; 5]`). What all of these types share is that their exact size is known at compile time, so by default they're stored directly on the stack — no heap allocation or pointer indirection needed, which makes access fast. This contrasts with dynamically-sized data like `String` and `Vec<T>`, which live on the heap — a contrast you'll meet again when `String` shows up in Lesson 8.
Connect it to a real scenario
In the content analyzer tool, return each file's statistics as a `(String, u32, f64)` tuple — file name, word count, and average word length carried together as one group. To track how many lessons exist per difficulty level, a `[u32; 3]` array (indexed for Basic, Intermediate, Advanced) works well, and knowing its size at compile time keeps bounds-checking efficient at runtime. Declare word count fields as `u32` (unsigned, since negative counts are never valid) rather than `i32` — `i32` would fail to enforce at the type level that a negative count is conceptually impossible.
Try the working example
fn main() {
let stats: (String, u32, f64) = (String::from("ownership.md"), 842, 5.3);
let (name, word_count, avg_len) = stats;
println!("{name}: {word_count} words, avg length {avg_len}");
let lessons_per_level: [u32; 3] = [7, 7, 6];
println!("Basic lessons: {}", lessons_per_level[0]);
}You print "ownership.md: 842 words, avg length 5.3" and "Basic lessons: 7".5-minute try-it
Create a `(String, u32)` tuple and a `[u32; 5]` array — destructure the tuple's two values and print three of the array's elements by index.
One important caution
Blanket-using the default integer type (`i32`) for count/length fields that can logically never be negative — `u32` would enforce that constraint at the type level, but habit often leads to `i32` everywhere.
Trying to access an array index beyond its compile-time size (like index 3 on a `[u32; 3]`) — Rust catches this with a runtime panic, but it's still a logic error the compiler cannot catch ahead of time.