Closures as Function Parameters

A closure is an anonymous function that can capture variables from the scope where it is defined, and Rust lets you pass closures into other functions as parameters so the caller can plug in custom behavior. This is how methods like sort_by, filter, and map work, and it is a pattern you will use constantly once you start writing your own generic helpers. Because Rust has no garbage collector, the compiler must know exactly how a closure interacts with the variables it captures, so every closure automatically implements one or more of three special traits — Fn, FnMut, and FnOnce — and a function that accepts a closure parameter must say which of these traits it requires. Understanding this trio is the key to writing (and reading) any function that takes a closure.

Overview: How Closures as Parameters Work

When you write a closure, Rust looks at what it does with the variables it captures and infers an unnamed, compiler-generated type for it — a small struct holding the captured variables, with a callable body attached. That type automatically implements one, two, or all three of the closure traits, depending on how it treats its captures:

  • Fn — the closure only reads its captured variables (or captures nothing). It can be called any number of times through a shared reference (&self).
  • FnMut — the closure mutates at least one captured variable. It can still be called any number of times, but it needs a mutable reference (&mut self) to do so.
  • FnOnce — the closure moves a captured variable out of itself (for example, returning an owned String it captured, or passing an owned value on to another function that takes it by value). Because the value is consumed, the closure can only be called once (it takes self by value).

These traits form a hierarchy: every closure that implements Fn also implements FnMut and FnOnce, and every FnMut closure also implements FnOnce. Think of it like a work order handed to a contractor. An Fn closure is a photocopied instruction sheet — you can hand out as many copies as you like and each one just gets read. An FnMut closure is a shared checklist — every time it is used, a box gets ticked, so you need exclusive access to it while using it. An FnOnce closure is a one-time meal voucher — using it consumes it, so it cannot be handed over again afterward.

When you write a function that accepts a closure, you choose the trait bound based on what your function needs to do with the closure, not on what the caller’s closure happens to do. If your function only ever calls the closure once, bound it with FnOnce — this is the most permissive choice, because every closure (whether it only reads, mutates, or consumes captures) satisfies FnOnce. If you need to call the closure repeatedly and it may need to mutate its own captured state, bound it with FnMut. If you need to call it repeatedly and only need read access, bound it with Fn — this is the most restrictive choice for you as the function author, but it is exactly what predicate-style closures (like a filter condition) usually satisfy anyway. Choosing the loosest bound your function can get away with makes your function usable with the widest range of caller closures.

Syntax

There are four common ways to write a function parameter that accepts a closure. They are functionally related but suited to different situations.

// Trait bound syntax (most common, generic over the closure's concrete type)
fn call_with_one<F: Fn(i32) -> i32>(f: F) -> i32 {
    f(1)
}

// where clause syntax (same meaning, easier to read with multiple/complex bounds)
fn call_with_one_where<F>(f: F) -> i32
where
    F: Fn(i32) -> i32,
{
    f(1)
}

// impl Trait syntax (shorthand, idiomatic for a single simple bound)
fn call_with_one_impl(f: impl Fn(i32) -> i32) -> i32 {
    f(1)
}

// trait object syntax (for heterogeneous closures or when the
// concrete closure type cannot be named, e.g. a struct field)
fn call_boxed(f: Box<dyn Fn(i32) -> i32>) -> i32 {
    f(1)
}
  • F: Fn(i32) -> i32F is a generic type parameter that stands for whatever concrete closure type the caller passes in; the bound restricts it to types implementing Fn with that specific signature.
  • impl Fn(i32) -> i32 — anonymous shorthand for the same generic bound; you cannot refer to the type F elsewhere in the signature with this form.
  • Box<dyn Fn(i32) -> i32> — a trait object: the closure is heap-allocated and called through a vtable, so the function no longer needs to know the concrete closure type at compile time.
  • The parentheses-and-arrow part, (i32) -> i32, describes the closure’s own parameter list and return type, exactly like a function signature.

Examples

Example 1: A simple Fn parameter

fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
    f(value)
}

fn main() {
    let double = |x: i32| x * 2;
    let result = apply(double, 5);
    println!("Result: {}", result);
}

Output:

Result: 10

The closure double captures nothing from its environment, so it automatically implements Fn (as well as FnMut and FnOnce). It is passed by value into apply, which calls it once with value and returns the result.

Example 2: An FnMut parameter that mutates captured state

fn apply_n_times<F: FnMut()>(mut f: F, times: u32) {
    for _ in 0..times {
        f();
    }
}

fn main() {
    let mut count = 0;
    apply_n_times(|| {
        count += 1;
        println!("Count is now {}", count);
    }, 3);
    println!("Final count: {}", count);
}

Output:

Count is now 1
Count is now 2
Count is now 3
Final count: 3

The closure passed to apply_n_times mutates count, so it captures count by mutable reference and only implements FnMut, not Fn. Notice the parameter is declared mut f: F inside apply_n_times — calling an FnMut closure requires a mutable reference to it, so the binding itself must be mutable. Once apply_n_times returns, the closure (and the mutable borrow of count it held) is dropped, so main can freely read count again afterward.

Example 3: An FnOnce parameter that consumes a captured value

fn consume<F: FnOnce() -> String>(f: F) {
    let s = f();
    println!("Consumed: {}", s);
}

fn main() {
    let name = String::from("Rust");
    let greet = move || format!("Hello, {}!", name);
    consume(greet);
}

Output:

Consumed: Hello, Rust!

The move keyword forces the closure to take ownership of name instead of borrowing it, so name is no longer usable in main after greet is defined. Bounding consume with FnOnce is the right choice here because it only ever calls the closure a single time; this also happens to be the most permissive bound, since it accepts closures that only borrow as well as ones that move their captures.

Example 4: impl Trait return value and boxed closures

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    let add5 = make_adder(5);
    println!("5 + 3 = {}", add5(3));

    let operations: Vec<Box<dyn Fn(i32) -> i32>> = vec![
        Box::new(|x| x + 1),
        Box::new(|x| x * 2),
        Box::new(|x| x * x),
    ];

    for op in &operations {
        println!("Result: {}", op(4));
    }
}

Output:

5 + 3 = 8
Result: 5
Result: 8
Result: 16

make_adder returns a closure using impl Fn(i32) -> i32, since each call to make_adder produces a closure of a distinct, unnameable type, but they all implement the same trait. In main, three different closures (each a different concrete type) are boxed into Box<dyn Fn(i32) -> i32> trait objects so they can live together in one Vec — something a generic Vec<F> could never do, since a single generic parameter can only represent one concrete type at a time.

How It Works Step by Step

When the compiler sees a generic parameter like F: Fn(i32) -> i32, it performs monomorphization: for every distinct closure type passed at a call site, it generates a separate compiled copy of the function with that concrete type baked in. This means calling the closure inside the function is a direct, inlinable call with zero runtime overhead — the same performance as if you had written the closure’s body directly inline. This is what people mean when they call closures a "zero-cost abstraction" in Rust.

By contrast, Box<dyn Fn(i32) -> i32> uses dynamic dispatch: the box stores a pointer to the closure’s data on the heap plus a pointer to a vtable (a small table of function pointers). Calling the closure means looking up the right function pointer at runtime and jumping to it. This costs a small amount of performance compared to monomorphization, but it buys flexibility — you can store closures of different concrete types together, as in Example 4’s Vec, or return different closures from different branches of an if without their types matching.

For the FnMut and FnOnce cases, the "self" parameter of the trait’s call method tells you what access the function needs: Fn::call takes &self, FnMut::call_mut takes &mut self, and FnOnce::call_once takes self by value. That is exactly why calling an FnMut closure repeatedly requires a mut binding (you need a fresh mutable reference each time), and why an FnOnce closure can only be invoked once (calling it consumes it, just like calling any function that takes self by value consumes the receiver).

Common Mistakes

Mistake 1: Using a variable after it was moved into a closure

fn consume<F: FnOnce() -> String>(f: F) {
    let s = f();
    println!("Consumed: {}", s);
}

fn main() {
    let name = String::from("Rust");
    let greet = move || format!("Hello, {}!", name);
    consume(greet);
    println!("{}", name);
    // error[E0382]: borrow of moved value: `name`
}

The move closure takes ownership of name, so name is no longer a valid binding in main after greet is created — trying to print it afterward is a compile error, not a runtime bug, because the borrow checker tracks moves statically. Clone the value beforehand if you need both the closure and the original:

fn consume<F: FnOnce() -> String>(f: F) {
    let s = f();
    println!("Consumed: {}", s);
}

fn main() {
    let name = String::from("Rust");
    let name_for_closure = name.clone();
    let greet = move || format!("Hello, {}!", name_for_closure);
    consume(greet);
    println!("Original name still available: {}", name);
}

Output:

Consumed: Hello, Rust!
Original name still available: Rust

Mistake 2: Forgetting mut on an FnMut parameter

fn apply_n_times<F: FnMut()>(f: F, times: u32) {
    for _ in 0..times {
        f();
    }
}

fn main() {
    let mut count = 0;
    apply_n_times(|| {
        count += 1;
    }, 3);
    println!("{}", count);
}
// error[E0596]: cannot borrow `f` as mutable, as it is not declared as mutable

Calling an FnMut closure requires a mutable reference to it, so the local binding f must itself be declared mut, even though the function’s own signature already says F: FnMut(). Add mut to the parameter:

fn apply_n_times<F: FnMut()>(mut f: F, times: u32) {
    for _ in 0..times {
        f();
    }
}

fn main() {
    let mut count = 0;
    apply_n_times(|| {
        count += 1;
    }, 3);
    println!("{}", count);
}

Output:

3

Mistake 3: Passing an FnMut closure where Fn is required

fn apply<F: Fn(i32) -> i32>(f: F, value: i32) -> i32 {
    f(value)
}

fn main() {
    let mut total = 0;
    let accumulate = |x: i32| {
        total += x;
        total
    };
    let result = apply(accumulate, 5);
    println!("Result: {}", result);
}
// error[E0525]: expected a closure that implements the `Fn` trait,
// but this closure only implements `FnMut`

accumulate mutates total, so it only implements FnMut, but apply demands a closure that implements the stricter Fn. The fix is to relax the function’s bound to match what it actually needs to accept:

fn apply<F: FnMut(i32) -> i32>(mut f: F, value: i32) -> i32 {
    f(value)
}

fn main() {
    let mut total = 0;
    let accumulate = |x: i32| {
        total += x;
        total
    };
    let result = apply(accumulate, 5);
    println!("Result: {}", result);
}

Output:

Result: 5

Best Practices

  • Choose the loosest trait bound your function actually needs: FnOnce if you call the closure at most once, FnMut if you call it repeatedly and it may mutate its captures, Fn only if you call it repeatedly and never need mutation.
  • Use impl Fn(...) -> T in a parameter or return position when you have one simple bound and don’t need to name the type parameter elsewhere.
  • Use the explicit F: Fn(...) -> T generic-parameter form (or a where clause) when you need multiple bounds, need to reference F more than once, or the bound is long.
  • Reach for Box<dyn Fn(...) -> T> only when you genuinely need to store different closure types together (a Vec of callbacks, a struct field holding a closure) or return different closures from different branches — it costs a small amount of indirection compared to a generic parameter.
  • Avoid an unnecessary move keyword when a closure only needs to borrow its captures; adding move needlessly can force values out of scope in the caller earlier than necessary.
  • Reserve .unwrap() inside a closure body for cases where failure is truly impossible or a bug; prefer propagating errors with ? or returning an Option/Result from the closure itself.

Practice Exercises

  • Write a generic function twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 that applies f to x two times in a row (i.e. f(f(x))) and returns the result. Test it with a closure that adds 3, and confirm it prints the expected value for an input of 10.
  • Write a function retry<F: FnMut() -> bool>(mut action: F, attempts: u32) -> bool that calls action up to attempts times, stopping early and returning true the moment action() returns true, or returning false if every attempt failed. Use a closure that captures a mutable counter and only returns true on its third call to verify the early-stop behavior.
  • Write a function that builds a Vec<Box<dyn Fn(i32) -> i32>> containing three different closures (for example, negate, square, and double), then loops over the vector applying each one to the number 6 and printing every result.

Summary

  • Closures automatically implement Fn, FnMut, and/or FnOnce depending on how they use their captured variables: read-only, mutating, or consuming.
  • Fn implies FnMut implies FnOnce — every closure implements at least FnOnce, and the strictest closures implement all three.
  • Bound a function parameter with the loosest closure trait your function actually needs, so it accepts the widest range of caller closures.
  • Generic parameters (F: Fn(...) -> T or impl Fn(...) -> T) use monomorphization for zero-cost static dispatch; Box<dyn Fn(...) -> T> uses a vtable for dynamic dispatch, trading a little speed for the ability to store heterogeneous closures.
  • An FnMut closure binding, and any function parameter typed as FnMut, must be declared mut to be callable.
  • A move closure takes ownership of the variables it uses, invalidating the originals in the enclosing scope — clone beforehand if you still need the original afterward.