Closures as Function Parameters
Functions can accept closures as parameters using three mechanisms: generic trait bounds (impl Fn or <F: Fn>), trait object references (&dyn Fn / &mut dyn FnMut), or function pointers (fn).
1. Static Dispatch with Generic Bounds (impl Fn / <F: Fn>)
Using impl Fn(...) or a generic type parameter <F: Fn(...)> monomorphizes the function for each concrete closure passed to it, enabling the compiler to inline the closure body with zero runtime indirection:
// Concise `impl Trait` parameter syntax:
fn filter_sum(numbers: &[i32], predicate: impl Fn(i32) -> bool) -> i32 {
let mut sum = 0;
for &n in numbers {
if predicate(n) {
sum += n;
}
}
sum
}
fn main() {
let data = [3, 8, 12, 5, 19, 20];
let min_val = 10;
let result = filter_sum(&data, |x| x >= min_val);
println!("Sum of elements >= {min_val}: {result}");
}
Sum of elements >= 10: 51
2. Dynamic Dispatch with Trait Object References (&dyn Fn / &mut dyn FnMut)
When a function is large, recursive, or stored in a vtable—or when you want to avoid generating multiple monomorphized copies of the function in the compiled binary—accept the closure behind a trait object reference (&dyn Fn or &mut dyn FnMut):
struct EventBus<'a> {
listeners: Vec<&'a dyn Fn(&str)>,
}
impl<'a> EventBus<'a> {
fn new() -> Self {
Self {
listeners: Vec::new(),
}
}
fn subscribe(&mut self, listener: &'a dyn Fn(&str)) {
self.listeners.push(listener);
}
fn publish(&self, event: &str) {
for listener in &self.listeners {
listener(event);
}
}
}
fn main() {
let tag = "AUDIT";
let logger = |msg: &str| println!("[{tag}] {msg}");
let metrics = |msg: &str| println!("[METRICS] len={}", msg.len());
let mut bus = EventBus::new();
bus.subscribe(&logger);
bus.subscribe(&metrics);
bus.publish("user_login");
}
[AUDIT] user_login
[METRICS] len=10
3. Function Pointers (fn(Args) -> Ret)
When a callback never needs to capture state from its environment (such as a C FFI callback or a static dispatch table), use a function pointer type fn(...):
type BinaryOp = fn(i32, i32) -> i32;
fn compute(a: i32, b: i32, op: BinaryOp) -> i32 {
op(a, b)
}
fn main() {
println!("Add: {}", compute(6, 4, |x, y| x + y));
println!("Sub: {}", compute(6, 4, |x, y| x - y));
}
Add: 10
Sub: 2
Comparing the Three Parameter Styles
| Parameter Type | Accepts Capturing Closures? | Binary Code Size | Call Overhead |
|---|---|---|---|
F: Fn(A) -> R / impl Fn(A) -> R |
Yes | One copy per closure call site | Zero (inlined) |
&dyn Fn(A) -> R / &mut dyn FnMut(A) -> R |
Yes | Single shared function body | Indirect vtable call |
fn(A) -> R |
No (non-capturing only) | Single shared function body | Indirect function pointer call |