04 - Functions¶
What this session is¶
About 45 minutes. You'll learn to define functions, the distinction between expressions and statements (Rust takes this seriously), and the basics of how to return values without writing return.
The shape¶
Concrete:
Note:
- Parameters need explicit types: x: i32.
- Return type follows ->. For void-equivalent (no return), omit the arrow (or write -> () - () is the "unit" type).
- The function body is x * 2 (no semicolon, no return). The last expression of a block is its value.
Expressions vs statements¶
This is the bit that takes adjusting.
- Statements perform an action and don't return a value.
let x = 5;is a statement. - Expressions produce a value.
5,5 + 5,f(x),if cond { a } else { b }are all expressions.
In Rust, a block ({ ... }) is an expression. Its value is whatever the last expression inside is, as long as that expression doesn't end with a semicolon.
let x = {
let a = 2;
let b = 3;
a + b // no semicolon - this is the block's value
};
println!("{x}"); // 5
If you put a semicolon:
let x = {
let a = 2;
let b = 3;
a + b; // semicolon - makes it a statement; block evaluates to ()
};
x is now () (unit), not 5. The compiler will probably complain elsewhere.
For functions: omit the semicolon on the last expression to return its value.
You can use return explicitly:
Both work. The implicit form is more idiomatic. Use return for early returns:
fn safe_divide(a: i32, b: i32) -> i32 {
if b == 0 {
return 0; // early return
}
a / b // normal return - no semicolon
}
Multiple parameters¶
Each parameter gets its own type annotation.
No defaults; no named arguments¶
Rust doesn't have default parameter values or named arguments. To get default-like behavior, write multiple functions or accept an Option/struct:
fn greet(name: &str) -> String {
format!("Hello, {name}")
}
fn greet_with(name: &str, greeting: &str) -> String {
format!("{greeting}, {name}")
}
(There are libraries that simulate named args via builder patterns - for now, multiple functions is fine.)
Returning nothing¶
Functions that return nothing have no ->:
Equivalently: -> () is the explicit form. () ("unit") is Rust's "nothing" - a type with exactly one value, also written ().
Returning multiple values: tuples¶
Like Python, Rust returns tuples for "several values at once":
fn divide(a: i32, b: i32) -> (i32, i32) {
(a / b, a % b)
}
fn main() {
let (q, r) = divide(17, 5);
println!("quotient {q}, remainder {r}");
}
(i32, i32)is the return type - a 2-tuple of integers.(a / b, a % b)constructs the tuple.let (q, r) = ...destructures it.
Tuples are anonymous - for a return type with named fields, use a struct (page 05).
Functions calling functions¶
fn square(x: i32) -> i32 { x * x }
fn sum_of_squares(a: i32, b: i32) -> i32 {
square(a) + square(b)
}
fn main() {
println!("{}", sum_of_squares(3, 4)); // 25
}
Variable scope¶
Variables exist only inside the block ({ }) they were declared in:
fn double(x: i32) -> i32 {
let result = x * 2;
result
}
fn main() {
println!("{result}"); // ERROR - `result` doesn't exist out here
}
Where to put functions¶
In a single-file program, all functions live at the top level of main.rs. Order doesn't matter - Rust scans the whole file, so functions can call each other regardless of which is defined first.
fn main() {
println!("{}", helper(5)); // OK even though helper is defined below
}
fn helper(x: i32) -> i32 { x + 1 }
In multi-file projects, you'll organize into modules (page 11).
Exercise¶
In your project, replace src/main.rs:
- Write
fn is_even(n: i32) -> bool. Use%. - From
main, printis_even(4)andis_even(7). Expected:true,false. - Write
fn count_evens(max: i32) -> i32that counts even numbers in1..=max. Loop, callis_even. - Print
count_evens(10)→5;count_evens(100)→50. - Stretch: write
fn quotient_and_remainder(a: i32, b: i32) -> (i32, i32). Frommain, destructure:let (q, r) = quotient_and_remainder(17, 5);and print both.
What you might wonder¶
"Why do I have to think about expressions vs statements?"
Rust uses "everything is an expression" pervasively - if, match, blocks. The semicolon-or-no-semicolon distinction is small but consistent. After a week you stop thinking about it.
"Can I omit the return type?"
For functions with no return value, yes (fn foo() { ... }). For functions that return something, no - you must declare it explicitly.
"Can a function call itself?" Yes (recursion). Common for tree traversal etc. The compiler doesn't currently optimize tail calls; very deep recursion can stack-overflow.
"Can I have two functions with the same name?" Not in the same scope. Use different names or methods on different types (page 05).
Done¶
- Define functions with parameters and return types.
- Distinguish expressions from statements.
- Return implicitly (no semicolon) or with
return. - Return multiple values via tuples; destructure with
let (a, b) = ....
Next page: structs and enums - Rust's way to make your own types.