Checkpoint: Rust Basics
Reviewed & published by Brayan K
This checkpoint consolidates everything from the basics track — Cargo, operators, tuples and arrays, formatting, pattern matching, conversions, constants, and ordering — into one multi-step program you build yourself.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll review each idea quickly, then tackle a real build challenge that combines parsing, sorting, and formatting, with a full worked solution and a quiz to confirm it stuck.
What This Checkpoint Covers
- Cargo workflow and the crate/module model
- Operators and expression-oriented control flow
- Tuples, arrays, slices, and Vec
- String formatting with the {} placeholder language
- Pattern matching, conversions, const/static
- Comparison, ordering, and sorting
1️⃣ Quick Recap of the Basics Track
Here's a single program that exercises one idea from each earlier lesson — destructuring, expression-based if, matching a parsed Result, a const with formatted output, and a sort. If every line makes sense, you're in great shape for the build challenge.
fn main() {
// Tuples + destructuring (Tuples & Arrays)
let (a, b) = (12, 30);
// Operators: everything is an expression
let bigger = if a > b { a } else { b };
println!("bigger = {}", bigger);
// Pattern matching over a parsed value (Conversions + Matching)
let parsed = "7".parse::<i32>();
match parsed {
Ok(n) => println!("parsed {}", n),
Err(_) => println!("bad number"),
}
// Constants + formatting (Const/Static + String Formatting)
const TAX: f64 = 0.08;
let price = 50.0;
println!("with tax: {:.2}", price * (1.0 + TAX));
// Sorting (Comparison & Ordering)
let mut v = vec![3, 1, 2];
v.sort();
println!("sorted = {:?}", v);
}📋 The Eight Skills in One Table
| Topic | Remember |
|---|---|
| Cargo | cargo run / fmt / clippy |
| Operators | if/blocks are expressions; no ++ |
| Tuples/Arrays | t.0 [T; N] &v[1..3] |
| Formatting | {:<6} {:>6.1} {:?} |
| Matching | exhaustive match; if let / let else |
| Conversions | as / From / TryFrom / parse |
| Const/Static | const inlines; static has one address |
| Ordering | sort_by_key, cmp, Reverse |
2️⃣ Build Challenge: A Scoreboard Report
Now put it together. You'll parse messy input into structs (skipping bad lines), sort with a tie-breaker, and print an aligned, formatted report with an average. This single task uses tuples/Options, conversions, ordering, and formatting at once.
Build Challenge
Read the step-by-step plan in the comments, then implement it. Try it fully before opening the solution below.
// 🎯 BUILD CHALLENGE: a scoreboard report.
//
// You're given raw lines like "Ada:90". Some lines are malformed and must
// be skipped. Build a clean, sorted, formatted report.
//
// STEP BY STEP:
// 1. Define struct Record { name: String, score: u32 } (derive Debug).
// 2. Write fn parse_record(line: &str) -> Option<Record>
// - split the line on ':' (look at str::split_once)
// - parse the score; return None if either part is missing/invalid
// 3. In main, parse every raw line, KEEPING only the valid ones
// (filter_map is perfect for Option-returning parsers).
// 4. Sort the records by score DESCENDING, breaking ties by name ascending.
// hint: b.score.cmp(&a.score).then(a.name.cmp(&b.name))
// 5. Print an aligned table: a header, each "rank.name" left in width 6
// and the score right in width 6, then an average row to 1 decimal.
//
// ✅ Target output:
// Name Score
// ------------
// 1.Ada 90
// 2.Cy 85
// 3.Bo 72
// 4.Di 72
// ------------
// Avg 79.8
fn main() {
let raw = ["Ada:90", "Bo:72", "broken-line", "Cy:85", "Di:72"];
// build the report here
}One clean way to do it. The ? operator inside parse_record turns a failed split or parse into an early None, and filter_map drops those. This compiles and produces exactly the target output:
📝 Checkpoint Quiz
Answer each in your head, then expand to check. No peeking first!
cargo check. It runs the compiler's analysis but skips code generation, so it's much faster than cargo build for catching errors while you iterate.
The trailing semicolon after 42 turns it into a statement, so the block has no final expression and yields the unit value (). Drop the semicolon ({ 42 }) and the block evaluates to 42.
It produces 44, because the cast wraps the bits to fit a u8. Use u8::try_from(300), which returns an Err instead of silently losing data.
Use let else when you want to bind the inner value for the rest of the function and bail out early on failure — its else block must diverge (return/break/panic). if let is for doing something only inside the matched block, with the binding scoped to that block.
Floats only implement PartialOrd, not Ord, because NaN is unordered. sort requires Ord. Use sort_by(|a, b| a.total_cmp(b)) instead.
A const has no fixed address — it's inlined at every use site. A static has one fixed memory location and the 'static lifetime, lasting the whole program. Prefer const unless you specifically need a single shared address.
Common Errors (and the fix)
- ❌ "cannot use the `?` operator in a function that returns ..." — ? needs the function to return Option/Result. ✅ Make parse_record return Option<Record>.
- ❌ "mismatched types" in the average — Integer division truncated. ✅ Cast with total as f64 / len as f64.
- ❌ Columns don't line up — Inconsistent widths. ✅ Use the same {:<6}/{:>6} specs on every row, including the header.
- ❌ Ties sort unpredictably — No tie-breaker. ✅ Chain .then(...) in your comparator.
Pro Tips
- 💡 Let parsers return Option/Result and use filter_map to keep only the good values in one pass.
- 💡 Compose orderings with .then(...) for clean, deterministic multi-key sorts.
- 💡 Run cargo clippy on your solution — it often suggests a more idiomatic version of working code.
🎉 Checkpoint Complete!
- ✅ You combined parsing, conversions, ordering, and formatting in one program
- ✅ filter_map keeps only valid parsed records
- ✅ sort_by with .then(...) gives deterministic, multi-key ordering
- ✅ Format specs build clean, aligned reports
Practice quiz
Which Cargo command type-checks your code quickly without producing a runnable binary?
- cargo build
- cargo run
- cargo check
- cargo fmt
Answer: cargo check. cargo check runs the compiler's analysis but skips code generation, so it is much faster than cargo build for catching errors while iterating.
In Rust, what does the block 'let x = { 42; };' evaluate to?
- the unit value ()
- 42
- a compile error
- None
Answer: the unit value (). The trailing semicolon after 42 turns it into a statement, so the block has no final expression and yields the unit value (). Drop the semicolon to get 42.
What does '300_i32 as u8' produce?
- 300
- 255 (clamped)
- a compile error
- 44 (wrapped bits)
Answer: 44 (wrapped bits). The as cast wraps the bits to fit a u8, giving 44. Use u8::try_from(300) to get an Err instead of silently losing data.
How do you destructure the tuple '(12, 30)' into two bindings a and b?
- let [a, b] = (12, 30);
- let (a, b) = (12, 30);
- let a, b = (12, 30);
- let {a, b} = (12, 30);
Answer: let (a, b) = (12, 30);. Tuples are destructured with parentheses: let (a, b) = (12, 30); binds a to 12 and b to 30.
When matching an Option, when is 'let else' a better fit than 'if let'?
- When you want to bind the inner value for the rest of the function and bail out early on failure
- When you only need the value inside a small scoped block
- When the Option is always Some
- let else cannot be used with Option
Answer: When you want to bind the inner value for the rest of the function and bail out early on failure. let else binds the inner value for the remaining function body and its else block must diverge (return/break/panic). if let scopes the binding to the matched block only.
Why can't you call '.sort()' directly on a Vec<f64>?
- f64 has no comparison at all
- sort only works on integers
- f64 implements PartialOrd but not Ord because NaN is unordered
- Vec<f64> is not allowed
Answer: f64 implements PartialOrd but not Ord because NaN is unordered. Floats implement only PartialOrd (NaN is unordered), and sort requires Ord. Use sort_by(|a, b| a.total_cmp(b)) instead.
What is the key difference between a const and a static in Rust?
- const is mutable, static is not
- const is inlined at each use site; static has one fixed memory address
- They are completely interchangeable
- static can only hold integers
Answer: const is inlined at each use site; static has one fixed memory address. A const is inlined at every use site with no fixed address, while a static has one fixed memory location and the 'static lifetime for the whole program.
Why does 'filter_map' pair so well with a parser that returns Option<Record>?
- It keeps the None values and discards the Some values
- It panics on any None
- It only works with Result, not Option
- It both drops the None values and unwraps the Some values in one pass
Answer: It both drops the None values and unwraps the Some values in one pass. filter_map keeps only the Some values, unwrapping them, while silently dropping the Nones, filtering and extracting in a single pass.
In 'b.score.cmp(&a.score).then(a.name.cmp(&b.name))', what does .then(...) do?
- It always uses the second ordering
- It returns the first ordering unless it is Equal, then falls through to the second
- It reverses both orderings
- It panics if the first ordering is Equal
Answer: It returns the first ordering unless it is Equal, then falls through to the second. Ordering::then returns the first ordering unless it is Equal, in which case it uses the second — a clean way to express a tie-breaker.
Which format placeholder left-aligns a value in a field width of 6?
- {:>6}
- {:^6}
- {:<6}
- {:6.0}
Answer: {:<6}. {:<6} left-aligns in a width of 6; {:>6} right-aligns and {:^6} centers. The '<' chooses left alignment.
Continue this course
- Previous: Comparison & Ordering (PartialEq, Ord)
- Next: Trait Objects & dyn — Dynamic dispatch with dyn Trait and Box<dyn Trait> vs generics
- Quick reference: Rust cheat sheet
Frequently asked questions
What should I be comfortable with before moving past this checkpoint?
You should be able to scaffold and run a project with Cargo, use Rust's operators knowing that if and blocks are expressions, group data with tuples and arrays, and format output with the println! placeholder language. You should also be able to branch on a value's shape with match, if let, and let else; convert between types using as, From/Into, TryFrom, and parse; declare const and static values; and compare and sort data with PartialEq, Ord, and the sort family. The build challenge on this page touches every one of those, so completing it on your own is a good signal you're ready.
Why does filter_map fit a parser that returns Option so well?
filter_map takes a closure that returns an Option for each element and keeps only the Some values, unwrapping them, while silently dropping the Nones. Because parse_record returns Option<Record> — Some for a valid line and None for a malformed one — filter_map both filters out the bad lines and extracts the good Records in a single pass. That's cleaner than calling map to get a Vec of Options and then a second step to remove and unwrap them. The same pattern works beautifully with parse, which also returns a Result you can convert to Option with .ok().
How does sort_by with then break ties cleanly?
sort_by takes a comparator returning an Ordering. The Ordering type has a then method that returns the first ordering unless it's Equal, in which case it falls through to the second. So b.score.cmp(&a.score).then(a.name.cmp(&b.name)) sorts primarily by score descending — note the arguments are flipped to reverse the order — and only when two scores are equal does it compare names ascending to decide the tie. Chaining then (or then_with for lazily computed keys) lets you express multi-level sort rules in one readable expression.
I'm comfortable with these basics — what comes next?
The next lesson moves into trait objects and dynamic dispatch with dyn, which lets you store and call different concrete types behind a common trait interface at runtime. From there the course builds toward the parts of Rust that make it distinctive — deeper ownership and borrowing patterns, generics and trait bounds, error handling with Result and the ? operator, and iterators and closures. Everything you consolidated here is the foundation those topics build on, so a solid grasp of this checkpoint will make the rest feel much smoother.