05 — Functions
Basics
fn add(a: i32, b: i32) -> i32 {
a + b // last expression — no semicolon — is the return value
}
fn no_return() {
println!("returns ()");
}
- The last expression (without
;) is the return value. - A trailing
;makes it a statement returning(). return x;is for early returns; the implicit last-expression form is idiomatic for the common case.
Statements vs Expressions
let x = (let y = 5;); // ERROR: statements don't produce values
let y = {
let z = 5;
z + 1 // expression — block evaluates to 6
};
Blocks { ... } are expressions. if, match, loop are also expressions.
Parameters & Patterns
Parameters can be patterns:
fn print_pair((a, b): (i32, i32)) { println!("{a} {b}"); }
fn first((a, _): (i32, i32)) -> i32 { a }
Diverging Functions (-> !)
fn forever() -> ! {
loop {}
}
fn die() -> ! {
panic!("bye");
}
! coerces to any type, allowing it anywhere a value is expected:
let v: i32 = match opt {
Some(x) => x,
None => die(), // ! coerces to i32
};
Default & Optional Parameters?
Rust has no function overloading or default parameters. Use:
- Builder pattern
- Multiple methods (
new,with_capacity) - Traits for "overloading" semantics (e.g.,
From/Into)
Generic Functions (preview)
fn first<T>(v: &[T]) -> Option<&T> {
v.first()
}
fn max<T: PartialOrd + Copy>(a: T, b: T) -> T {
if a > b { a } else { b }
}
impl Blocks (Methods)
struct Rect { w: u32, h: u32 }
impl Rect {
fn area(&self) -> u32 { self.w * self.h } // method
fn new(w: u32, h: u32) -> Self { Rect { w, h } } // associated fn
fn set(&mut self, w: u32) { self.w = w; } // mut borrow
}
&self=self: &Self(immutable borrow).&mut self= mutable borrow.self(by value) = consumesself.- Associated functions (no
self) called asRect::new(...)(like static methods).
Self and self Keywords
Self is the type the impl is for. self is the receiver shorthand. Self in a trait body refers to the implementing type.
Variadic Functions
Only extern "C" FFI functions can be C-style variadic:
extern "C" {
fn printf(fmt: *const u8, ...) -> i32;
}
Idiomatic variadic-ness comes from macros (println!, vec!) or slices (fn sum(nums: &[i32])).
Function Pointers vs Closures
fn add(a: i32, b: i32) -> i32 { a + b }
let fp: fn(i32, i32) -> i32 = add; // function pointer, Copy, Sized
let cl = |a, b| a + b; // closure, captures env, !Sized
See the Closures chapter for Fn/FnMut/FnOnce distinctions.
Recursion
Rust doesn't guarantee tail-call optimization. Deep recursion can overflow the stack. For deep/iterative algorithms, convert to an explicit loop with a stack.
fn fact(n: u64) -> u64 {
if n == 0 { 1 } else { n * fact(n - 1) }
}
const fn
Compile-time-callable functions with a restricted feature set:
const fn square(x: i32) -> i32 { x * x }
const N: i32 = square(5); // evaluated at compile time
Each release expands what's allowed in const fn (loops, mutable locals, etc.).
Calling Conventions & ABI
extern "C" fn c_fn(x: i32) -> i32 { x + 1 }
extern "Rust" fn rust_fn(x: i32) -> i32 { x + 1 } // default
extern "C" { fn imported(x: i32) -> i32; }
Useful for FFI and callbacks passed to C libraries.
Edge Cases
returnin a closure:returninside a closure returns from the closure, not the enclosing function (unlike some languages). Use labeled loops/breaks or?carefully.- Block-as-expression footgun: forgetting the trailing
;returns the value; adding it silently changes the return type to(). The compiler catches this. fntypes areCopy: you can copy function pointers freely; closures are not necessarilyCopy.- Lifetime elision in fn signatures:
fn first(s: &str) -> &strhas elided lifetimes; the compiler infers one input lifetime → output lifetime. - Recursion + generics: monomorphized per type — code bloat risk.
#[inline]: a hint;#[inline(always)]can bloat code; usually trust the compiler.
Summary
Functions are expressions, support patterns in parameters, can diverge, have no overloading, and methods live in impl blocks. Next: control flow.