Closures vs. Functions
Both functions (fn) and closures (|...|) represent callable blocks of code, but they differ in how they interact with their surrounding scope, how their types are represented in memory, and how they are passed as values.
Comparison Table
| Feature | Named Function (fn) |
Closure (|...|) |
|---|---|---|
| Captures enclosing local variables | No — can only access parameters and items/statics | Yes — captures by &T, &mut T, or value (T) |
| Type annotations on signature | Required | Optional (inferred from usage) |
| Type name in source code | Function item type (coerces to fn(A) -> R pointer) |
Unique anonymous compiler-generated type; constrained via Fn, FnMut, FnOnce |
Size in memory (size_of_val) |
0 bytes (function item) or 8 bytes (fn pointer on 64-bit) |
Sum of sizes of its captured fields (0 bytes if non-capturing) |
| Supports generic type parameters | Yes (fn foo<T>(x: T)) |
No (|x| is monomorphic once inferred) |
Environment Capture Prohibition on fn Items (E0434)
An inner fn declared inside another function cannot reference local variables from the outer function, whereas a closure captures them seamlessly:
// ❌ does not compile
fn main() {
let factor = 3;
fn scale(x: i32) -> i32 {
x * factor
}
}
error[E0434]: can't capture dynamic environment in a fn item
--> src/main.rs:6:13
|
6 | x * factor
| ^^^^^^
|
= help: use the `|| { ... }` closure form instead
Coercing Non-Capturing Closures to Function Pointers (fn)
A closure that does not capture any variables from its environment has the same runtime representation as a plain function and automatically coerces to a function pointer type (fn(Args) -> Ret):
fn apply_op(a: i32, b: i32, op: fn(i32, i32) -> i32) -> i32 {
op(a, b)
}
fn named_add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
// 1. Passing a named function:
println!("Named fn: {}", apply_op(10, 4, named_add));
// 2. Passing a non-capturing closure (coerces to `fn(i32, i32) -> i32`):
println!("Closure -> fn: {}", apply_op(10, 4, |x, y| x * y));
}
Named fn: 14
Closure -> fn: 40
If a closure captures even one variable from its environment, it cannot coerce to a fn pointer (E0308), because a plain code pointer has no place to store the captured environment data.
Passing Named Functions Where Closure Traits Are Expected
Because all named functions and function pointers (fn(A) -> R) implement Fn, FnMut, and FnOnce, you can pass a named function or method path (such as str::trim or i32::abs) anywhere a closure bound is expected:
fn main() {
let raw = [" alpha ", "beta\n", " gamma"];
let cleaned: Vec<&str> = raw.into_iter().map(str::trim).collect();
println!("{cleaned:?}");
}
["alpha", "beta", "gamma"]