Functions & control flow
fn is the keyword. The last expression is the return — no
return needed (and idiomatic Rust omits it). if is an
expression that yields a value. match is exhaustive. There's one
loop keyword that's three loops in disguise.
1 · The intuition
Rust is expression-oriented. Almost everything yields a value:
if blocks, match blocks, even loops with break value.
The semicolon at end-of-line turns an expression into a statement (yielding
()). The absence of a semicolon at the end of a block makes that
block's value its result.
{ a + b } is a block expression
worth a + b. { a + b; } is a block expression worth
() — the unit type. This is why the last line of a function body
has no semicolon.2 · Functions
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = the function's return value
}
fn shout(msg: &str) -> String {
format!("{}!!!", msg.to_uppercase())
}
fn no_return() {
println!("done");
// implicit return of () — the unit type
}
fn main() {
let s = add(2, 3);
println!("{}", s);
println!("{}", shout("hi"));
no_return();
}Parameter types are always required. Return types follow ->;
omitted means () (unit, like void). Inside the body,
the last expression is the return — no return keyword needed.
return exists for early returns.
3 · if is an expression
fn main() {
let n = 7;
// if as an expression
let parity = if n % 2 == 0 { "even" } else { "odd" };
println!("{}", parity);
// chain
let bucket = if n < 5 {
"small"
} else if n < 10 {
"medium"
} else {
"large"
};
println!("{}", bucket);
}No parentheses around the condition. Every branch must produce the same type —
the type checker enforces it. There is no ternary because if already
is the ternary.
4 · match — the power tool
enum Status {
Active,
Pending,
Closed(String),
}
fn describe(s: &Status) -> String {
match s {
Status::Active => "user is active".to_string(),
Status::Pending => "waiting on confirmation".to_string(),
Status::Closed(reason) => format!("closed: {}", reason),
}
}
fn main() {
println!("{}", describe(&Status::Active));
println!("{}", describe(&Status::Closed("payment failed".into())));
}Status
and forget to handle it in match, the compiler refuses to build. This
is the single biggest correctness feature of Rust — refactor a type, fix every
site at compile time.5 · if let and while let
fn main() {
let some_value: Option<i32> = Some(42);
// Full match
match some_value {
Some(n) => println!("got {}", n),
None => (), // do nothing
}
// if let — equivalent when you only care about one variant
if let Some(n) = some_value {
println!("got {}", n);
}
// while let — loop while the pattern matches
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
}6 · Loops
fn main() {
// 1. Infinite loop with explicit break
let mut count = 0;
let result = loop {
count += 1;
if count == 5 { break count * 2; } // loops can yield values!
};
println!("loop result: {}", result);
// 2. while
let mut n = 3;
while n > 0 {
println!("n={}", n);
n -= 1;
}
// 3. for over a range
for i in 0..3 {
println!("i={}", i);
}
// 4. for over an iterator
for c in "ab".chars() {
println!("{}", c);
}
}loop can return a value. Use break expr;
to exit the loop and yield expr as the loop expression's value. Rare
but elegant for "retry until success" patterns.7 · Common mistakes
- Adding a semicolon to the last expression. Turns it into a statement, returns
()instead. The compiler complains: "expected i32, found ()". - Forgetting parens around
(0..n). Actually parens are optional.for i in 0..nworks.0..=nfor inclusive. - Returning early without a value.
return;means "return ()". To return a value early:return value;. - Non-exhaustive
match. Won't compile. Use_ => ...for "everything else". - Mixing
if letandelsepatterns. Possible since Rust 1.65 (let-else), but still surprises newcomers. Use full match when in doubt.
8 · Exercises (~10 min)
- FizzBuzz with match. Use
match (n % 3, n % 5)with tuple patterns. - Loop returns value. Write a function that loops, doubling a value, and breaks when it exceeds 100. Return the doubled value.
- Range madness. Print numbers 1-10 with
for, then 10-1 with.rev(), then evens with.step_by(2). - Drop the semi. Take a 5-line function with explicit
return. Rewrite withoutreturn, just trailing expressions.
9 · When it clicks
- You drop trailing
returnstatements by reflex. matchis your first reach for any enum or Option/Result.- You see the missing semicolon at the end of a function body as a feature, not a typo.
- You use
if letfor single-arm cases without thinking.