Level 1 · Sprout

Functions 🧑‍🍳

A function is a recipe: give it ingredients, get back a dish. Write it once, use it a thousand times.

Recipes with ingredients

You've been using a function since day one: main. Now let's write our own. Parameters (the ingredients) go in the parentheses, and in Rust, every parameter declares its type:

fn greet(name: &str, excitement: u8) {
    println!("Hello, {name}{}", "!".repeat(excitement as usize));
}

fn main() {
    greet("Rusty", 3);   // Hello, Rusty!!!
    greet("Chris", 1);   // Hello, Chris!
}

Why insist on types? Because it makes functions a contract. Anyone calling greet knows exactly what it needs, and the compiler slams the door on wrong ingredients before the program can run.

Giving something back: ->

The arrow declares what type comes back out:

fn double(n: i32) -> i32 {
    n * 2
}

fn main() {
    let answer = double(21);
    println!("{answer}"); // 42
}

Wait, where's the return? This is one of Rust's most elegant habits, and it deserves its own section.

The expression secret ✨

Rust code comes in two flavors:

The rule: if the last line of a function is an expression with no semicolon, that value is returned automatically. Adding a semicolon turns it into a statement, which returns nothing. One character changes everything:

fn double(n: i32) -> i32 {
    n * 2    // ✅ expression → returned
}

fn broken_double(n: i32) -> i32 {
    n * 2;   // ❌ statement → returns nothing!
}
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 | fn broken_double(n: i32) -> i32 {
  |    -------------            ^^^ expected `i32`, found `()`
  |
help: remove this semicolon to return this value

The compiler even knows the cure. ((), called "unit", is Rust's way of saying "nothing here.") The return keyword still exists for leaving a function early; for the final line, Rustaceans always use the bare expression.

Composing recipes

Functions calling functions is how programs grow without becoming spaghetti:

fn area(width: f64, height: f64) -> f64 {
    width * height
}

fn price_per_m2(total: f64, w: f64, h: f64) -> f64 {
    total / area(w, h)
}

fn main() {
    let cost = price_per_m2(1200.0, 4.0, 3.0);
    println!("You're paying {cost} per square meter");
}
⚠️ Common stumbles
  • A semicolon on the final expression of a returning function: see above, it's the classic.
  • Forgetting parameter types: fn greet(name) won't compile; Rust needs name: &str.
  • Type mismatch between what -> promises and what the body delivers; the error message will point right at it.
Exercise 1

Temperature translator

Write fn to_fahrenheit(celsius: f64) -> f64 using the formula c × 9 ÷ 5 + 32, and print the result for 100°C. (Remember: use 9.0 and 5.0; no mixing integers and floats!)

Reveal solution
fn to_fahrenheit(celsius: f64) -> f64 {
    celsius * 9.0 / 5.0 + 32.0
}

fn main() {
    println!("100°C = {}°F", to_fahrenheit(100.0)); // 212°F
}
Exercise 2

Spot the bug

Why doesn't this compile, and what's the one-character fix?

fn square(n: i32) -> i32 {
    n * n;
}
Reveal answer

The semicolon turns n * n into a statement, so the function returns () instead of the promised i32. Delete the semicolon and the last expression becomes the return value.

Exercise 3

Tip calculator

Write fn total_with_tip(bill: f64, tip_percent: f64) -> f64, then use it to print the total for a 84.50 bill with an 18% tip.

Reveal solution
fn total_with_tip(bill: f64, tip_percent: f64) -> f64 {
    bill + bill * tip_percent / 100.0
}

fn main() {
    let total = total_with_tip(84.50, 18.0);
    println!("Total: {total:.2}"); // {:.2} = two decimal places
}