Closures

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