Function and Closure Types
Understand the different types that represent callable code in Rust including function pointers and the Fn, FnMut, and FnOnce closure traits
Rust distinguishes between several kinds of callable things, each with its own type-level representation. The type system tracks exactly how a callable interacts with the values it captures from its environment—whether it only reads them, mutates them, or consumes them. This tracking happens through three traits (Fn, FnMut, FnOnce) and one concrete type (fn). Mastering these is the difference between fighting the borrow checker and letting it work for you when you pass behavior around.
The Function Pointer Type (fn)
In Rust, fn is a concrete type, not a trait. It represents a pointer to a function that does not capture any environment. Think of it like a pointer to a compiled function in memory, exactly like function pointers in C. The syntax mirrors closure parameter syntax, but uses the fn keyword directly in the type position.
fn add_one(x: i32) -> i32 {
x + 1
}
fn apply_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = apply_twice(add_one, 5);
println!("The answer is: {}", answer); // 12
}
apply_twice declares that its first parameter is a function pointer. You pass the name of an existing function (add_one) directly. No closure syntax, no generic type parameter—just a plain pointer. Because fn is a concrete type with a known size, it can be stored on the stack, placed in arrays, or passed across FFI boundaries without any indirection.
fn is a type, not a trait:
Unlike closures, fn is a concrete type. You don't need impl or dyn to use it. This makes fn the simplest callable to pass around when no capturing is needed.
Non-Capturing Closures Coerce to fn
A closure that captures nothing from its environment—often called a non-capturing closure—can be automatically converted to a function pointer. This is a convenience that avoids forcing you to write a named function for simple one-off logic that doesn't need outside data.
fn main() {
let operations: [fn(i32) -> i32; 3] = [
|x| x + 1,
|x| x * 2,
|x| x - 3,
];
let val = 10;
for op in &operations {
println!("{}", op(val)); // 11, 20, 7
}
}
Each closure here has no captures—the bodies only use the parameter x. The compiler allows each closure to become an fn and stores them directly in the array. If any closure tried to capture a local variable, even by reference, the coercion would fail and the type would be an anonymous closure type instead.
Capturing closures cannot be fn:
Even a read-only capture (e.g., |x| x + threshold) creates a closure that holds a reference to threshold. That closure is not an fn—it’s an anonymous type implementing Fn. Forcing a closure with captures into an fn context results in a compile error.
When fn is the Right Choice
Use fn when:
- You need to store multiple callables in a homogeneous collection (like an array or
Vec) without dynamic dispatch. - You are interfacing with C code that expects function pointers (C has no closures).
- Performance is critical and you know the callable will never need to capture anything.
fn pointers use static dispatch—the compiler knows exactly which function will be called, so calls can be inlined and optimized aggressively. There is no heap allocation or vtable lookup.
The Three Closure Traits: Fn, FnMut, FnOnce
Closures that capture environment variables get an anonymous, compiler-generated type. This type implements one or more of three traits depending on how the closure uses its captured values. The traits form a hierarchy: Fn is the most permissive for the caller (least demanding of the captured values), then FnMut, then FnOnce. Every closure that implements Fn also implements FnMut and FnOnce; every FnMut also implements FnOnce.
FnOnce– The closure can be called at least once. It may consume the values it captures (take ownership), so after one call the closure might be invalid. All closures implementFnOnce.FnMut– The closure can be called multiple times and requires a mutable reference to itself to do so. It may mutate its captured values but doesn't consume them (unless they areCopy). A closure that modifies a captured variable (e.g., incrementing a counter) isFnMut.Fn– The closure can be called multiple times using only an immutable reference to itself. It neither consumes nor mutates its captures. Most read-only closures fall into this category.
fn main() {
let mut count = 0;
let s = String::from("hello");
// Fn: only reads captured values (immutable reference to s)
let read_closure = || println!("{}", s);
read_closure();
read_closure(); // works multiple times
// FnMut: mutates captured count
let mut inc_closure = || {
count += 1;
count
};
println!("{}", inc_closure()); // 1
println!("{}", inc_closure()); // 2
// FnOnce: consumes captured s (moves it)
let consume_closure = move || {
let _owned = s;
};
consume_closure();
// consume_closure(); // error: use after move
}
The compiler infers the minimal trait bound needed for the closure’s body. If you use |x| x + 1 and don’t capture anything, you get a closure that implements all three. If you capture a variable and mutate it, the closure only implements FnMut and FnOnce.
Functions implement all closure traits:
Regular functions (via fn) automatically implement Fn, FnMut, and FnOnce. This means you can pass a named function anywhere a closure trait is expected, making APIs that accept impl Fn() backward-compatible with plain functions.
Trait Bound Syntax in Practice
When writing a function that accepts any callable, prefer a generic type parameter with a closure trait bound. This keeps the call static and allows both closures and function pointers.
fn process<F>(data: Vec<i32>, processor: F) -> Vec<i32>
where
F: Fn(i32) -> i32,
{
data.into_iter().map(processor).collect()
}
fn main() {
let numbers = vec![1, 2, 3];
let threshold = 2;
// Both closures and functions work
let result1 = process(numbers.clone(), |x| x * threshold); // closure with capture
let result2 = process(numbers, i32::abs); // function pointer
println!("{:?} {:?}", result1, result2);
}
This design gives maximum flexibility: the caller can pass whatever callable they want, and the compiler generates a specialized version of process for each concrete callable type, inlining aggressively.
Each closure is its own unique type:
Two closures with identical bodies and signatures are different types to the compiler. let c1 = |x| x + 1; and let c2 = |x| x + 1; produce two distinct anonymous types, even though both implement Fn(i32) -> i32. You cannot put them in the same Vec without boxing.
The Relationship Between fn and the Closure Traits
A function pointer (fn) implements all three closure traits. That means you can always use a named function where a closure is expected. However, the reverse is not true: a closure that captures anything cannot be turned into an fn.
This asymmetry leads to a key design guideline: accept closures via generic Fn traits unless you have a specific reason to require fn. By doing so, your code works with both closures and function pointers, and you don’t artificially restrict callers.
fn call_fn<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(x)
}
fn square(x: i32) -> i32 { x * x }
fn main() {
let cube = |x| x * x * x;
println!("{}", call_fn(square, 3)); // 9
println!("{}", call_fn(cube, 3)); // 27
}
Had call_fn taken fn(i32) -> i32 instead of a generic F: Fn(i32) -> i32, the cube closure would be rejected because it is not an fn.
Returning Closures
Returning a closure from a function is more involved than accepting one. A closure’s type is anonymous and has a compile-time size that the compiler must know. Writing -> Fn(i32) -> i32 fails because Fn is a trait, not a sized type. Two approaches exist:
Using impl Trait
With impl Trait syntax, you can return an opaque type that implements a closure trait. This works great when the function always returns the same closure type.
fn make_adder(increment: i32) -> impl Fn(i32) -> i32 {
move |x| x + increment
}
fn main() {
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15
}
The compiler creates a single concrete type for the returned closure and hides it behind the impl Fn interface. The caller can treat it as any Fn(i32) -> i32.
The limitation arises when you need to return different closures from the same function based on a runtime condition. The following won’t compile:
fn choose_behavior(kind: &str) -> impl Fn(i32) -> i32 {
if kind == "double" {
|x| x * 2
} else {
|x| x + 1
}
}
Compiler error:
error[E0308]: mismatched types
expected opaque type `impl Fn(i32) -> i32`
found different opaque type `impl Fn(i32) -> i32`
Even though both branches return closures with the same signature, each closure is a distinct anonymous type, and impl Trait requires a single concrete type for all return paths. Using impl Trait doesn’t erase the type; it just hides it. The compiler still needs one underlying type.
Using Box<dyn Fn>
When you need heterogeneous closures—different types that share a trait—use a trait object. This introduces dynamic dispatch via a vtable and a heap allocation.
fn choose_behavior(kind: &str) -> Box<dyn Fn(i32) -> i32> {
if kind == "double" {
Box::new(|x| x * 2)
} else {
Box::new(|x| x + 1)
}
}
fn main() {
let f = choose_behavior("double");
println!("{}", f(10)); // 20
}
The cost: each call goes through a pointer indirection and vtable lookup, preventing some compiler optimizations. But you gain the ability to store closures of different concrete types in the same variable or collection.
Box<dyn Fn> requires 'static lifetime by default:
Trait objects carry a lifetime. If the captured variables don't live long enough, you may need to use Box<dyn Fn() + 'a> to tie the lifetime to some scope. In many cases, closures that only capture owned values or 'static data can use Box<dyn Fn()>.
Common Pitfalls
Accidentally Restricting an API with fn
If you start a library function with fn parameters and later realize you need closures with captures, changing the signature is a breaking change. Prefer impl Fn() or F: Fn() from the start to keep the API flexible.
Expecting impl Trait Returns to Unify Distinct Closures
As shown earlier, each impl Trait return position creates a single hidden concrete type. Two different closures cannot share that opaque type. When you encounter the "expected opaque type, found a different opaque type" error, you need a trait object instead.
Forgetting That FnOnce Consumes the Closure
A common surprise is that a closure implementing FnOnce can only be called once. This often happens when the closure moves a captured value out, e.g., move || drop(some_string). If you need to call the closure multiple times, restructure to avoid consuming the captured values, or use FnMut / Fn.
Assuming All Fn Closures Can Coerce to fn
Only non-capturing closures coerce to function pointers. If the closure captures anything, even by shared reference, the coercion fails. The compiler error will mention that the closure implements Fn but isn't an fn. This is a frequent frustration when trying to store closures in a [fn(); N].
Choosing the Right Callable Type
The decision between fn, generic Fn traits, and Box<dyn Fn> boils down to three tradeoffs: capture requirement, performance needs, and type homogeneity.
| Requirement | Recommended Type | Notes |
|---|---|---|
| No captures, simple callable | fn | Fastest, C-FFI compatible, homogeneous storage |
| Captures, but one concrete type at a time | impl Fn(...) or F: Fn(...) | Static dispatch, zero-cost, flexible |
| Multiple closure types stored together | Box<dyn Fn(...)> | Dynamic dispatch, heap allocation, runtime polymorphism |
There is no one-size-fits-all, but a default strategy is to accept closures via generics and return them via impl Trait when possible, falling back to Box<dyn> when you need to handle multiple types.
Summary
Function and closure types in Rust encode how a callable interacts with its environment. fn is the simplest, representing a plain pointer to a function with no captures. The three closure traits—Fn, FnMut, FnOnce—form a hierarchy that tracks immutability, mutability, and ownership of captured data. The compiler infers the appropriate trait for each closure and ensures safety without runtime overhead. When you need to return closures, impl Trait gives you a zero-cost opaque type for a single implementation, while Box<dyn Fn> enables collections of different closures at the cost of dynamic dispatch. Understanding these types lets you design APIs that are both safe and flexible, letting callers pass behavior as easily as data.