Why this matters
You now have the whole basic toolkit: values, variables, functions, input, if, loops and match. Knowing the pieces is not the same as being able to build something with them. This lesson is about the step in between: taking a problem stated in words and turning it into a working program, with a method you can repeat. The exercises are small but realistic, the kind of thing you write constantly as a working programmer.
A method for small problems
- Restate the problem with examples. Write down a few inputs and the exact outputs, including at least one awkward case (zero, empty input, a negative number, the largest value).
- Find the shape. Is it "for each input line, do X"? "Search until found"? "Accumulate a total"? Most small programs are one of the loop shapes from earlier lessons.
- Split off functions for any part with a clear name:
is_prime,reverse,parse_command. A function can be reasoned about and tested on its own. - Write the simplest version, run it on your examples, then handle the awkward cases.
- Read the output carefully against the specification: spacing, order, the last line.
Let us apply it to three problems.
Worked example 1: greatest common divisor
Problem: read two positive integers, print their greatest common divisor.
Examples: (12, 18) gives 6; (17, 5) gives 1; (7, 7) gives 7.
Shape: a computation in a loop. Euclid's algorithm says gcd(a, b) = gcd(b, a % b), and gcd(a, 0) = a. Each step makes the numbers smaller, so it terminates.
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let r = a % b;
a = b;
b = r;
}
a
}
fn main() {
for (a, b) in [(12, 18), (17, 5), (7, 7), (1071, 462)] {
println!("gcd({a}, {b}) = {}", gcd(a, b));
}
}
gcd(12, 18) = 6
gcd(17, 5) = 1
gcd(7, 7) = 7
gcd(1071, 462) = 21
mut a: u64 in the parameter list makes the local copy of the parameter mutable; the caller's values are untouched. Trace one call by hand: (12, 18) becomes (18, 12), then (12, 6), then (6, 0): answer 6. Tracing by hand on paper is the single best debugging technique for loops.
Worked example 2: running statistics over input
Problem: read numbers from stdin, one per line, and print the count, minimum, maximum and average (two decimals). With no numbers, print no data.
Shape: accumulate over lines. Awkward cases: empty input (no minimum exists, and division by zero in the average), a single number, negative numbers.
use std::io;
fn main() {
let mut count = 0;
let mut sum: i64 = 0;
let mut min = i64::MAX;
let mut max = i64::MIN;
for line in io::stdin().lines() {
let line = line.unwrap();
let line = line.trim();
if line.is_empty() {
continue;
}
let x: i64 = line.parse().unwrap();
count += 1;
sum += x;
if x < min {
min = x;
}
if x > max {
max = x;
}
}
if count == 0 {
println!("no data");
return;
}
println!("count {count}, min {min}, max {max}, average {:.2}", sum as f64 / count as f64);
}
count 4, min -7, max 12, average 3.00
Starting min at i64::MAX and max at i64::MIN means the first number always replaces them; this is an alternative to "start with the first element" that works when you do not have the first element yet. return in main ends the program early (with exit status 0).
Worked example 3: a command interpreter
Problem: read commands like push 3, pop, show, and react to each.
The key new tool is splitting a line into words. line.split_whitespace() is an iterator over the words of a line (separated by any amount of spaces or tabs). Calling .next() on it yields the next word, wrapped in Some, or None when there are no more:
fn main() {
let line = " add 42 ";
let mut words = line.split_whitespace();
println!("{:?}", words.next());
println!("{:?}", words.next());
println!("{:?}", words.next());
}
Some("add")
Some("42")
None
.unwrap() takes the word out of the Some (and panics on None, a missing word). Combined with match on the command word, this gives a compact command loop:
use std::io;
fn main() {
let mut total = 0;
for line in io::stdin().lines() {
let line = line.unwrap();
let mut words = line.split_whitespace();
match words.next() {
Some("add") => {
let n: i32 = words.next().unwrap().parse().unwrap();
total += n;
}
Some("show") => println!("total = {total}"),
Some(other) => println!("unknown command {other}"),
None => {}
}
}
}
total = 42
unknown command jump
Matching directly on words.next() handles the empty line (None) and the command at once. Some("add") is a pattern that matches only a Some holding exactly "add"; Some(other) matches any other word and binds it. You are getting a preview of module 8 here; it is fine to use this pattern before you know every detail of Option.
Edge cases are the job
Look at how much of each example is about awkward cases: the empty line, the empty input, the first element, the missing word. That is typical. The "main" logic is often a few lines; correctness lives in the boundaries. The exercise checkers in this course always include such cases, so get used to listing them before you start.
Common mistakes
Printing separators wrongly. "Numbers separated by spaces" means no space before the first one. Print the separator before every item except the first, or collect the items and join them (module 9).
Resetting an accumulator inside the loop. let mut total = 0; inside the loop body creates a fresh zero on every pass. Accumulators are declared before the loop.
Parsing before checking. parse().unwrap() on a comment or empty line panics. Filter the lines you want to skip first.
Integer overflow in a search. d * d <= n with d and n as u64 is fine for any realistic input; with u32 near the top of its range, d * d would overflow and panic in a debug build.
Recap
- Restate the problem with concrete examples, including awkward ones, before writing code.
- Identify the loop shape; split named pieces into functions.
split_whitespace()plus.next()takes a line apart word by word;matchdispatches on words.- Trace loops by hand on small inputs; it finds bugs faster than staring.
- Most of the work in a correct program is in its edge cases.
Going further: extend the statistics program to print the median. You will need to store all the numbers, which needs a growable list: let mut v = Vec::new(); v.push(x); v.sort();. That is a preview of module 9, and a reason for the next module: once values live on the heap, Rust's ownership rules start to matter.