Rust’s function and closure system goes far beyond simple definitions. Functions are first-class values with their own types. Closures implement traits that precisely describe how they interact with their environment. Mastering these concepts unlocks powerful API design patterns and functional programming techniques.
Function Pointers (fn)
Functions in Rust have a concrete type: fn(ArgTypes) -> ReturnType. This is the function pointer type (lowercase fn). It refers to a specific function, not a closure, and captures nothing from its environment:
fn add_one(x: i32) -> i32 { x + 1 }
fn subtract_one(x: i32) -> i32 { x - 1 }
fn double(x: i32) -> i32 { x * 2 }
fn apply(f: fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
fn main() {
println!("{}", apply(add_one, 5)); // 6
println!("{}", apply(subtract_one, 5)); // 4
println!("{}", apply(double, 5)); // 10
// Store in array/vec
let operations: Vec<fn(i32) -> i32> = vec![add_one, double, subtract_one];
let result = operations.iter().fold(10, |acc, f| f(acc));
println!("{}", result); // ((10 + 1) * 2) - 1 = 21
// Pass named function where closure expected — fn satisfies Fn + FnMut + FnOnce
let doubled: Vec<i32> = vec![1, 2, 3].into_iter().map(double).collect();
println!("{:?}", doubled); // [2, 4, 6]
}
Function pointers are useful for:
- C FFI (C callbacks expect function pointers, not closures)
- Storing in
const/static(closures can’t beconst) - When you need the simplicity of no capture and no allocation
The Fn Trait Hierarchy
Every closure and function implements one or more of three traits. They form a hierarchy:
FnOnce ← FnMut ← Fn
(least strict) (most strict)
FnOnce: can be called at least once; may consume captured valuesFnMut: can be called many times; may mutate captured valuesFn: can be called many times concurrently; only immutably borrows
All Fn closures are also FnMut and FnOnce. All FnMut closures are also FnOnce.
fn call_once<F: FnOnce() -> String>(f: F) -> String { f() }
fn call_mut<F: FnMut() -> String>(mut f: F) -> String { f() }
fn call_ref<F: Fn() -> String>(f: F) -> String { f() }
fn main() {
let name = String::from("Rust");
// Fn — immutable borrow
let greet = || format!("Hello, {}!", name);
println!("{}", call_ref(&greet)); // Can call ref version
println!("{}", call_mut(greet)); // And mut version
// println!("{}", call_once(greet)); // And once version (already moved to call_mut)
// FnMut — mutable borrow
let mut count = 0;
let mut counter = || { count += 1; count };
println!("{}", call_mut(&mut counter)); // 1
println!("{}", call_mut(&mut counter)); // 2
// call_ref(&counter); // Error: requires Fn, but counter is FnMut
// FnOnce — takes ownership
let data = vec![1, 2, 3];
let consume = move || { let sum: i32 = data.iter().sum(); sum };
println!("{}", call_once(consume)); // 6
// call_once(consume); // Error: already consumed
}
Choosing the Right Bound
When you accept a closure, use the least restrictive bound your code needs:
// Accept Fn when you call it multiple times and only need immutable access
fn retry<F: Fn() -> Result<(), String>>(f: F, times: u32) {
for i in 0..times {
match f() {
Ok(()) => return,
Err(e) => eprintln!("Attempt {}: {}", i + 1, e),
}
}
}
// Accept FnMut when you call it multiple times but need mutation
fn run_n_times<F: FnMut()>(mut f: F, n: usize) {
for _ in 0..n { f(); }
}
// Accept FnOnce when you call it exactly once (most permissive for caller)
fn run_once<F: FnOnce() -> String>(f: F) -> String {
f()
}
Higher-Order Functions
Functions that take or return other functions:
fn compose<A, B, C>(f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> impl Fn(A) -> C {
move |x| g(f(x))
}
fn main() {
let trim_and_upper = compose(
|s: &str| s.trim().to_string(),
|s: String| s.to_uppercase(),
);
println!("{}", trim_and_upper(" hello world ")); // HELLO WORLD
// Building a pipeline
let parse_and_double = compose(
|s: &str| s.parse::<i32>().unwrap_or(0),
|n: i32| n * 2,
);
println!("{}", parse_and_double("21")); // 42
}
Currying
fn add(a: i32) -> impl Fn(i32) -> i32 {
move |b| a + b
}
fn multiply(a: i32) -> impl Fn(i32) -> i32 {
move |b| a * b
}
fn main() {
let add5 = add(5);
let triple = multiply(3);
println!("{}", add5(10)); // 15
println!("{}", triple(7)); // 21
let numbers = vec![1, 2, 3, 4, 5];
let result: Vec<i32> = numbers.iter().map(|&x| add5(triple(x))).collect();
println!("{:?}", result); // [8, 11, 14, 17, 20]
// Apply multiple transforms
let transforms: Vec<Box<dyn Fn(i32) -> i32>> = vec![
Box::new(add(10)),
Box::new(multiply(2)),
Box::new(add(-5)),
];
let value = transforms.iter().fold(1, |acc, f| f(acc));
println!("{}", value); // ((1 + 10) * 2) - 5 = 17
}
Returning Closures from Functions
Closures have anonymous types the compiler generates internally — you can’t name them. Two options for returning closures:
Option 1: impl Fn(...) — Zero Cost
The concrete type is fixed at compile time. Only one possible return type:
fn make_greeter(greeting: &str) -> impl Fn(&str) -> String + '_ {
move |name| format!("{}, {}!", greeting, name)
}
fn make_between_checker(low: i32, high: i32) -> impl Fn(i32) -> bool {
move |x| x >= low && x <= high
}
fn main() {
let hello = make_greeter("Hello");
println!("{}", hello("Alice")); // Hello, Alice!
println!("{}", hello("Bob")); // Hello, Bob!
let is_teen = make_between_checker(13, 19);
println!("{}", is_teen(15)); // true
println!("{}", is_teen(25)); // false
}
Option 2: Box<dyn Fn(...)> — When Type Must Be Erased
Use when you have multiple branches returning different closures:
enum Strategy { Aggressive, Conservative }
fn make_strategy(s: Strategy) -> Box<dyn Fn(i32) -> i32> {
match s {
Strategy::Aggressive => Box::new(|x| x * 3),
Strategy::Conservative => Box::new(|x| x + 1),
}
}
fn main() {
let strategies = vec![Strategy::Aggressive, Strategy::Conservative];
for strategy in strategies {
let f = make_strategy(strategy);
println!("{}", f(10)); // 30, then 11
}
}
Closures as Callbacks
A common pattern in event-driven and asynchronous code:
struct EventBus {
handlers: Vec<Box<dyn Fn(&str)>>,
}
impl EventBus {
fn new() -> Self { EventBus { handlers: Vec::new() } }
fn on<F: Fn(&str) + 'static>(&mut self, handler: F) {
self.handlers.push(Box::new(handler));
}
fn emit(&self, event: &str) {
for handler in &self.handlers {
handler(event);
}
}
}
fn main() {
let mut bus = EventBus::new();
bus.on(|e| println!("[Logger] Event: {}", e));
let prefix = "ALERT";
bus.on(move |e| println!("[{}] {}", prefix, e));
let mut count = 0;
// Note: for mutable capture, need Mutex or Cell for shared state in Fn
bus.on(|e| println!("[Counter] Processing: {}", e));
bus.emit("user.login");
bus.emit("order.placed");
}
Function Composition Pipeline
A type-safe pipeline builder using closures:
struct Pipeline<T> {
value: T,
}
impl<T> Pipeline<T> {
fn new(value: T) -> Self { Pipeline { value } }
fn pipe<U>(self, f: impl FnOnce(T) -> U) -> Pipeline<U> {
Pipeline { value: f(self.value) }
}
fn result(self) -> T { self.value }
}
fn main() {
let result = Pipeline::new(" hello, rust! ")
.pipe(|s| s.trim().to_string())
.pipe(|s| s.split(", ").map(String::from).collect::<Vec<_>>())
.pipe(|words| words.iter().map(|w| {
let mut chars = w.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
}
}).collect::<Vec<_>>())
.pipe(|words| words.join(" "))
.result();
println!("{}", result); // Hello Rust!
}
Practical API Design: The Builder Pattern with Closures
Closures enable ergonomic builder APIs:
struct RequestBuilder {
url: String,
method: String,
transform: Box<dyn Fn(String) -> String>,
}
impl RequestBuilder {
fn new(url: &str) -> Self {
RequestBuilder {
url: url.to_string(),
method: "GET".to_string(),
transform: Box::new(|s| s),
}
}
fn method(mut self, method: &str) -> Self {
self.method = method.to_string();
self
}
fn with_transform<F: Fn(String) -> String + 'static>(mut self, f: F) -> Self {
self.transform = Box::new(f);
self
}
fn build(self) -> String {
let base = format!("{} {}", self.method, self.url);
(self.transform)(base)
}
}
fn main() {
let request = RequestBuilder::new("/api/users")
.method("POST")
.with_transform(|s| s.to_uppercase())
.build();
println!("{}", request); // POST /API/USERS
}
fn vs impl Fn vs Box<dyn Fn> — Decision Guide
fn(T) -> U |
impl Fn(T) -> U |
Box<dyn Fn(T) -> U> |
|
|---|---|---|---|
| Captures environment | No | Yes | Yes |
const/static |
Yes | No | No |
| Runtime cost | Zero | Zero | Heap + vtable |
| Multiple return types | No | No | Yes |
| FFI compatible | Yes | No | No |
| Stored in struct | Yes | No (opaque) | Yes (explicit) |
// Use fn: stateless, const, FFI
const DOUBLE: fn(i32) -> i32 = |x| x * 2;
// Use impl Fn: most function parameters and return types
fn transform(data: &[i32], f: impl Fn(i32) -> i32) -> Vec<i32> {
data.iter().map(|&x| f(x)).collect()
}
// Use Box<dyn Fn>: stored in structs, heterogeneous collections, conditional returns
struct Handler { f: Box<dyn Fn(i32) -> String> }
Summary
fn(T) -> Uis a function pointer — no captures, zero size, FFI-compatibleFn,FnMut,FnOncedescribe how closures interact with captured state- Use the least restrictive bound (
FnoverFnMutoverFnOnce) in function signatures - Return
impl Fn(...)for zero-cost static dispatch;Box<dyn Fn(...)>when type erasure is needed - Function pointers implement all three
Fntraits — they can be used anywhere a closure is expected - Higher-order functions, composition, and currying are natural in Rust’s type system
Comments