Closures

Closures with Generic Bounds

In generic structs, trait implementations, and higher-order functions, closures can be combined with additional trait bounds—such as lifetime bounds (+ 'static), thread-safety bounds (+ Send + Sync), cloning bounds (+ Clone), and Higher-Ranked Trait Bounds (for<'a> Fn(&'a T)) for closures that accept references of any lifetime.

Storing a Closure Inside a Struct

To store a closure inside a struct without dynamic dispatch (Box<dyn Fn>), parameterize the struct over a type parameter F and add the Fn* bound on the impl block:

struct RetryRunner<F> {
    max_attempts: u32,
    operation: F,
}

impl<F, T, E> RetryRunner<F>
where
    F: FnMut(u32) -> Result<T, E>,
{
    fn new(max_attempts: u32, operation: F) -> Self {
        Self {
            max_attempts,
            operation,
        }
    }

    fn execute(&mut self) -> Result<T, E> {
        let mut attempt = 1;
        loop {
            match (self.operation)(attempt) {
                Ok(val) => return Ok(val),
                Err(err) if attempt >= self.max_attempts => return Err(err),
                Err(_) => attempt += 1,
            }
        }
    }
}

fn main() {
    let mut runner = RetryRunner::new(3, |attempt| {
        if attempt < 3 {
            println!("Attempt {attempt} failed, retrying...");
            Err("transient failure")
        } else {
            Ok(format!("Succeeded on attempt {attempt}"))
        }
    });

    println!("Result: {:?}", runner.execute());
}
Attempt 1 failed, retrying...
Attempt 2 failed, retrying...
Result: Ok("Succeeded on attempt 3")

Notice the parentheses around (self.operation)(attempt): without parentheses, self.operation(attempt) looks for a method named operation on RetryRunner rather than invoking the closure stored in the field self.operation.

Combining Closure Bounds (Fn + Send + Sync + 'static)

Concurrent registries, async task spawners, and web router handlers combine Fn with Send + Sync + 'static so handlers can be stored in shared state and invoked simultaneously across worker threads:

use std::sync::Arc;

type SharedHandler = Arc<dyn Fn(&str) -> String + Send + Sync + 'static>;

fn make_handler(prefix: String) -> SharedHandler {
    Arc::new(move |req: &str| format!("{prefix}: {req}"))
}

fn main() {
    let handler = make_handler(String::from("200 OK"));
    println!("{}", handler("GET /health"));
}
200 OK: GET /health

Higher-Ranked Trait Bounds (for<'a> Fn(&'a str) -> &'a str)

When a closure parameter is a reference whose lifetime comes from inside the callee (for example, a temporary buffer created inside a loop) rather than a lifetime fixed before the function call, the bound must work for all possible lifetimes 'a. Rust expresses this with a Higher-Ranked Trait Bound (for<'a>), which is also the implicit desugaring whenever you write a reference in an Fn bound without an explicit lifetime:

// Writing `F: Fn(&str) -> &str` automatically desugars to:
fn apply_to_local_buffer<F>(transform: F) -> String
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let local_string = String::from("   higher-ranked bound   ");
    // `local_string` only lives inside this function, so `'a` is local:
    let slice: &str = transform(&local_string);
    slice.to_uppercase()
}

fn main() {
    // Annotating the closure parameter type `&str` helps the compiler infer `for<'a>`:
    let result = apply_to_local_buffer(|s: &str| s.trim());
    println!("Result: '{result}'");
}
Result: 'HIGHER-RANKED BOUND'