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:
- An expression produces a value:
5,n * 2,double(21), even anif(soon!). - A statement does something and produces nothing:
let x = 5;
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");
}
- 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 needsname: &str. - Type mismatch between what
->promises and what the body delivers; the error message will point right at it.
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
}
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.
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
}