A whole program, before any rules
This chapter does something that sounds reckless: it shows you a complete, working program before teaching you a single rule of the language. That is the whole pedagogy of Part I. You are going to learn to read Rust first, the way you can follow the gist of a sentence in a language you are still learning, long before you could diagram its grammar.
A word before you start. Over the next few pages you will meet terms like Option, match, filter_map, and derive without being told the rules behind them, and somewhere around the third one you may feel slightly over your skis, as if the chapter is moving faster than you can follow. That feeling is intended, not a sign you have fallen behind. Part I shows you the shape of the language before any of its specifics, and shape is meant to be recognized, not yet understood. Most fluent Rust programmers felt some version of this unease at the start, and it passes quickly: each idea you only glimpse here gets its own patient chapter later. Let the shapes wash over you for now, run the little programs, and trust that the detail is coming.
Here is the program. It reads lines of metric data like cpu 0.42, throws away anything malformed, and prints a small summary. Read it top to bottom once; just notice its shape. Then press Run (and click into the code to change it and run again):
#[derive(Debug)]
struct Sample {
name: String,
value: f64,
}
fn parse(line: &str) -> Option<Sample> {
let (name, value) = line.split_once(' ')?;
let value = value.trim().parse::<f64>().ok()?;
Some(Sample {
name: name.to_string(),
value,
})
}
fn main() {
let raw = "\
cpu 0.42
mem 0.91
cpu 0.55
disk 0.73
mem not-a-number
cpu 0.88";
let samples: Vec<Sample> = raw.lines().filter_map(parse).collect();
let count = samples.len();
let sum: f64 = samples.iter().map(|s| s.value).sum();
let peak = samples.iter().max_by(|a, b| a.value.total_cmp(&b.value));
println!("parsed {count} samples");
println!("average load: {:.2}", sum / count as f64);
if let Some(p) = peak {
let level = match p.value {
v if v >= 0.9 => "critical",
v if v >= 0.7 => "warning",
_ => "ok",
};
println!("peak: {} at {:.2} ({level})", p.name, p.value);
}
}It prints three lines: parsed 5 samples, the average load, and the peak. Five samples, not six: one input line was mem not-a-number, and the program dropped it without a word and without crashing. Section 04 is about why that is so easy to do safely in Rust.
A few pieces in there run ahead of Part I on purpose, the total_cmp (floating-point numbers need a special total ordering to be compared), the .ok(), the ::<f64>. Do not try to decode those yet; each gets its own chapter later. What the next six sections name is what you will be able to read by the end.
Not to make you write this program. To make you recognize its parts on sight: the expressions, the match, the Option, the pipeline, the struct. The next six sections name them one at a time, each with a small program you can run and change.
Almost everything is an expression
In most languages there is a hard line between a statement, which does something, and an expression, which evaluates to a value. Rust mostly erases that line. An if, a match, and even a plain { } block all evaluate to a value. Here, the value of size comes straight out of a match. Run it, then change n and run again:
fn main() {
let n = 7;
let size = match n {
0..=5 => "small",
_ => "big",
};
println!("{n} is {size}");
}The whole match is one expression: it runs, produces one of the strings, and that string is what size binds to. The first program does this to choose a severity, let level = match p.value { ... }. The same idea is why a function can end with a value and no return: the last expression in a block is its value.
match: one door for every case
A match takes one thing and lists what to do for each possibility, each as a pattern, then =>, then a result. The if attached to an arm is a guard; the _ is the catch-all. What makes it more than a prettier ladder is that it must be exhaustive: the compiler refuses to build if you leave a case unhandled.
Here is a match that forgets a case. Press Run: the compiler refuses to build and tells you exactly what is missing. Add a final _ => "many", arm and Run again; it prints. (Click into the code to edit; the editor flags the gap live as you type.)
fn main() {
let n = 3;
let name = match n {
1 => "one",
2 => "two",
};
println!("{name}");
}In most languages a switch with no matching case and no default simply does nothing, silently, at runtime. Rust treats an unhandled case as an error you must see now. It feels strict the first time; you will come to lean on it.
Option: absence, made part of the type
Recall the bad line that vanished without a crash. The trick is a type called Option: when a value might not be there, Rust writes the maybe-ness into the type itself. Parsing text that is a number gives Some(number); text that is not gives None. Here are three strings, one of them junk. Run it:
fn main() {
let inputs = ["42", "not a number", "7"];
let numbers: Vec<i32> = inputs
.iter()
.filter_map(|s| s.parse().ok())
.collect();
println!("kept {numbers:?}");
}It prints kept [42, 7]: the junk string was simply not kept. That is filter_map at work, the step named in the reassurance up top. It runs each item through a function that returns an Option, keeps every Some, and silently drops every None. The .ok() is what turns a parse that might fail into exactly that Option: Some on success, None on failure.
There is no null in Rust. Back in the first program this is the whole story of the vanishing line: parse returns Option<Sample>, the ? inside it bows out early with None the moment a split or parse fails, and raw.lines().filter_map(parse) keeps only the lines that came back Some. The malformed mem not-a-number never made it into the results.
Pipelines: data through small steps
The busiest lines in Rust are chains of small steps joined by dots. This shape is not unique to Rust: if you have used array methods in JavaScript, streams in Java, or LINQ in C#, you have met it before. Go is the exception, with no built-in equivalent, so where Go reaches for a for loop to build up a result, Rust reaches for a chain like this. Read it left to right: take the numbers, keep the even ones, multiply each by ten, add them up. Then change the filter or the map and run it again:
fn main() {
let nums = [1, 2, 3, 4, 5, 6];
let total: i32 = nums
.iter()
.copied()
.filter(|n| n % 2 == 0)
.map(|n| n * 10)
.sum();
println!("total = {total}");
}The piece in vertical bars, like |n| n * 10, is a closure: a small inline function you hand to a step. The filter_map from the last section is one of these steps too. And the chain is lazy: nothing actually happens until a final step like sum (or collect) asks for the results. The first program computes its average load with exactly this shape, samples.iter().map(|s| s.value).sum().
Structs and derive: shaping the data
A struct groups related fields under a name. The line above it, #[derive(Debug)], is an attribute: a note to the compiler asking it to generate, for free, the code that lets the value be printed for inspection with {:?}. Run this, then delete the #[derive(Debug)] line and watch the compiler explain why the print no longer works:
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 3, y: 4 };
println!("{p:?}");
}You got a capability by asking for it, instead of writing it by hand. The first program opens with this exact pattern, #[derive(Debug)] sitting above struct Sample. How methods attach to a struct, and what Debug actually is underneath, are Part III stories.
The #[…] syntax looks alien because it does something most languages hide: it hands the compiler structured instructions about your code. Read #[derive(Debug)]as “compiler, please generate the debug-printing for this type.” You will meet attributes for tests, for configuration, and more; they always sit just above the thing they describe.
Read it again
Now scroll back to the program in section 01 and read it once more. The strange punctuation has names. You can see the struct, the function that might return nothing, the pipeline, the expression that decides a severity, the macro that prints. You can read it, which is the entire goal of Part I, and you have run and bent the shapes it is built from. A few details still run ahead, and they get their own chapters; recognizing the shape was the point.
Rust code is expressions that produce values, matches that cover every case, types that admit when a value can be absent, and pipelines that move data through small named steps, over structs the compiler can be asked to teach new tricks.
From here, Part I slows down. The next chapter, Values, Not Exceptions, takes the first idea you only glimpsed, that absence and failure are ordinary values rather than special control flow, and gives it room to breathe.
The whole chapter in six lines
- Part I teaches you to read Rust before you write it.
- Almost everything is an expression.
if,match, and blocks produce values. matchlists a result per case and must be exhaustive.- There is no
null. A maybe-missing value has typeOption, and?bails onNone. - Pipelines chain small lazy steps with closures like
|n| n * 10. - Structs group fields, and attributes like
#[derive(Debug)]generate abilities for free.