03 / 20 · Day 1
Day 1 · Concept 03

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.

The semicolon rule. { 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

rust src/main.rs
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

rust src/main.rs
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

rust src/main.rs · match exhaustively
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())));
}
Exhaustiveness. If you add a new variant to 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

rust src/main.rs · single-case sugar
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

rust src/main.rs · three 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..n works. 0..=n for 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 let and else patterns. Possible since Rust 1.65 (let-else), but still surprises newcomers. Use full match when in doubt.

8 · Exercises (~10 min)

  1. FizzBuzz with match. Use match (n % 3, n % 5) with tuple patterns.
  2. Loop returns value. Write a function that loops, doubling a value, and breaks when it exceeds 100. Return the doubled value.
  3. Range madness. Print numbers 1-10 with for, then 10-1 with .rev(), then evens with .step_by(2).
  4. Drop the semi. Take a 5-line function with explicit return. Rewrite without return, just trailing expressions.

9 · When it clicks

  • You drop trailing return statements by reflex.
  • match is 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 let for single-arm cases without thinking.
Found this useful?