Functions in Rust
Learn fn syntax, return values, closures, and higher-order functions in Rust.
Defining Functions
Functions are the primary unit of code reuse in Rust. Every Rust program starts at main, and you build complexity by composing functions. The fn keyword introduces a function definition; parameter types and the return type are always explicit in the signature, so the function’s contract is always clear without needing to read the body.
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon — this expression is the return value
}
fn main() {
let result = add(3, 4);
println!("{}", result); // 7
}
Key rules:
- Parameter types are always required — no inference in function signatures.
- Return type is declared after
->. - The last expression (without
;) is the implicit return value. returnexits early with an explicit value.
Implicit vs Explicit Return
Rust uses the last expression in a function body as the return value when no semicolon is present. This expression-based approach means functions can be written concisely, but it also means that accidentally adding a semicolon changes the return type to () (unit), which the compiler will catch.
fn max(a: i32, b: i32) -> i32 {
if a > b {
return a; // early return with explicit keyword
}
b // implicit return — last expression, no semicolon
}
fn main() {
println!("{}", max(10, 7)); // 10
println!("{}", max(3, 9)); // 9
}
Adding a semicolon after the last expression changes its type to () (unit), which causes a compile error if the function declares a non-unit return type:
fn broken() -> i32 {
42; // ERROR: expected i32, found () — the semicolon makes this a statement, not an expression
}
Functions with No Return Value
Functions that exist purely for side effects — printing, modifying state, writing to a file — return the unit type (). You do not need to write -> () explicitly; it is the default when no return type is declared.
fn print_separator(ch: char, count: usize) {
// std::iter::repeat builds a repeating iterator; collect() assembles it into a String
let line: String = std::iter::repeat(ch).take(count).collect();
println!("{}", line);
}
fn main() {
print_separator('-', 40);
println!("Hello, Rust!");
print_separator('=', 40);
}
Returning Multiple Values via Tuples
Rust functions return a single value, but that value can be a tuple carrying multiple pieces of data. This is the standard pattern when a function needs to return a result along with some status information, before reaching for a dedicated struct.
fn divide(a: f64, b: f64) -> (f64, bool) {
if b == 0.0 {
(0.0, false) // return a sentinel value and a success flag
} else {
(a / b, true)
}
}
fn main() {
let (result, ok) = divide(10.0, 3.0);
if ok {
println!("{:.4}", result); // 3.3333
}
let (_, ok) = divide(5.0, 0.0); // _ ignores the value we don't need
println!("valid: {}", ok); // valid: false
}
Function Pointers
Functions are first-class values in Rust. You can store them in variables, pass them as arguments, and collect them in arrays. The type of a function pointer is written fn(ParamType) -> ReturnType.
fn double(x: i32) -> i32 { x * 2 }
fn square(x: i32) -> i32 { x * x }
// accept any function with signature fn(i32) -> i32
fn apply(f: fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
fn main() {
println!("{}", apply(double, 5)); // 10
println!("{}", apply(square, 5)); // 25
// Store function pointers in a Vec and iterate over them
let ops: Vec<fn(i32) -> i32> = vec![double, square];
for op in &ops {
println!("{}", op(4));
}
// 8
// 16
}
Closures
Closures are anonymous functions that capture variables from their surrounding scope. They are defined with |params| body and are central to idiomatic Rust — most iterator methods take closures as arguments. Because closures capture context, they are more flexible than plain function pointers for callbacks and transformations.
fn main() {
let multiplier = 3;
// The closure captures `multiplier` by reference from the enclosing scope
let triple = |x: i32| x * multiplier;
println!("{}", triple(5)); // 15
println!("{}", triple(10)); // 30
// multiplier is still usable here because the closure only borrowed it
}
Closures infer their parameter and return types from usage, which keeps them concise:
fn main() {
let add = |a, b| a + b; // types inferred as i32
let greet = |name| format!("Hello, {}!", name); // name inferred as &str
println!("{}", add(2, 3)); // 5
println!("{}", greet("Alice")); // Hello, Alice!
}
Closure Traits: Fn, FnMut, FnOnce
Rust categorises closures by how they capture their environment, and each category maps to a trait. This matters when you write generic functions that accept closures — the trait bound you choose determines how callers can use the closure.
| Trait | Captures | Can call |
|---|---|---|
FnOnce | Takes ownership of captured variables | Once only |
FnMut | Mutably borrows captured variables | Multiple times |
Fn | Immutably borrows captured variables | Multiple times |
fn call_once<F: FnOnce() -> String>(f: F) {
println!("{}", f()); // f is consumed here — cannot call again
}
fn call_mut<F: FnMut()>(mut f: F) {
f(); f(); f(); // called three times — f must be FnMut or stronger
}
fn call_fn<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x) // can call any number of times — f must be Fn
}
fn main() {
let name = String::from("Rust");
// move transfers ownership of name into the closure — it becomes FnOnce
call_once(move || format!("Hello from {}!", name));
let mut count = 0;
// Captures count by mutable reference — FnMut
call_mut(|| count += 1);
println!("count: {}", count); // 3
// Captures factor by immutable reference — Fn
let factor = 5;
println!("{}", call_fn(|x| x * factor, 6)); // 30
}
Higher-Order Functions
Functions that take or return other functions are called higher-order functions. They are a powerful abstraction tool — apply_twice below works with any transformation, and make_adder produces specialised functions on demand. The move keyword transfers ownership of captured variables into the returned closure so the closure can outlive the function that created it.
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x)) // applies the function to x, then applies it again to the result
}
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n // `move` transfers n into the closure so it owns its data
}
fn main() {
let double = |x| x * 2;
println!("{}", apply_twice(double, 3)); // 12 (3*2=6, 6*2=12)
let add5 = make_adder(5);
let add10 = make_adder(10);
println!("{}", add5(3)); // 8
println!("{}", add10(3)); // 13
}
Iterator Methods with Closures
The most common use of closures in real Rust code is with iterator adapters. The iterator pipeline pattern — chaining filter, map, fold, and similar methods — produces code that is both concise and efficiently compiled: the chain is fused into a single loop with no intermediate allocations.
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Chain: keep only even numbers, square each, collect into a Vec
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0) // keep evens: 2, 4, 6, 8, 10
.map(|&x| x * x) // square each: 4, 16, 36, 64, 100
.collect(); // materialise into a Vec
println!("{:?}", result); // [4, 16, 36, 64, 100]
// fold reduces a sequence to a single value
let sum = numbers.iter().fold(0, |acc, &x| acc + x);
println!("sum: {}", sum); // 55
// any / all — short-circuit predicates
let has_even = numbers.iter().any(|&x| x % 2 == 0); // true
let all_positive = numbers.iter().all(|&x| x > 0); // true
println!("{} {}", has_even, all_positive);
}
Diverging Functions
A function that never returns has the return type !, called “never”. The compiler treats ! as compatible with any type, so you can use a diverging function in any expression position — including inside if arms or match arms that need to produce a value. panic!, todo!, and std::process::exit all have type !.
fn fatal_error(msg: &str) -> ! {
eprintln!("Fatal: {}", msg);
std::process::exit(1); // process exits — this function truly never returns
}
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
fatal_error("division by zero"); // OK: ! coerces to f64 in this branch
}
a / b
}
Const Functions
Functions marked const fn can be evaluated at compile time when called with constant arguments. This lets you compute lookup tables, magic constants, or validation checks without any runtime cost — the result is baked directly into the binary.
const fn factorial(n: u64) -> u64 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}
// Computed entirely at compile time — no runtime computation needed
const FACT_10: u64 = factorial(10);
fn main() {
println!("{}", FACT_10); // 3628800
}