A function factory
You've been writing Rust for a few weeks. You can make functions, you can make closures. Now you want to combine them: a function that builds a closure tailored to its arguments, then hands that closure to the caller. A counter factory. A logger that pre-fills the source name. A predicate that holds onto a threshold. The pattern is everywhere in real code.
You try the obvious thing:
fn make_adder(x: i32) -> Fn(i32) -> i32 {
|y| x + y
}
The compiler scolds you in three different ways at once. Each scolding is a clue about something specific in Rust's type system. Let's untangle them.
Closures don't have a normal type
The first thing to know: every closure you write has its own anonymous, compiler-generated struct. Two closures with identical signatures still have different types. That's why you can't write Fn(i32) -> i32 as the return type directly. Fn is a trait, not a type. It describes a behaviour ("this thing can be called like a function"), not a fixed shape with a known size.
Because the size isn't known up front, the bare Fn(i32) -> i32 doesn't fit on the stack. Rust needs a strategy for hiding the closure's actual type. There are two.
The modern, lightweight strategy: impl Trait in the return position.
The older, heap-allocating strategy: Box<dyn Trait>.
The minimal working version
// `impl Fn(i32) -> i32` says: "some concrete type the compiler knows,
// which implements Fn(i32) -> i32. The caller doesn't need to know what it is."
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// `move` forces the closure to capture `x` by value.
// Without it, the closure would borrow `x`, which dies at the end of make_adder.
move |y| x + y
}
fn main() {
let add_ten = make_adder(10);
println!("{}", add_ten(5)); // 15
println!("{}", add_ten(7)); // 17
}
Three details deserve attention.
impl Fn(...) in the return type means the function returns a single, specific closure type. The compiler picks the concrete type at compile time. The caller treats it as a black box that can be called with one i32 and returns an i32.
move makes the closure take ownership of x instead of borrowing it. Without move, the closure would hold a reference to x. But x lives in make_adder's stack frame, which is gone by the time anyone tries to call the closure. The borrow checker spots this and stops you with an error like:
error[E0373]: closure may outlive the current function, but it borrows `x`,
which is owned by the current function
--> src/main.rs:3:5
|
3 | |y| x + y
| ^^^ - `x` is borrowed here
| |
| may outlive borrowed value `x`
help: to force the closure to take ownership of `x`, use the `move` keyword:
|
3 | move |y| x + y
That's the textbook fix. Add move, problem gone.
The closure body captures x and adds whatever y arrives. Each call to make_adder produces a new closure that has internalised its own x.
Fn vs FnMut vs FnOnce
You'll see three closure traits. They form a hierarchy based on how the closure uses its captures.
Fn closures only read their captured variables. They can be called any number of times.
FnMut closures mutate something they captured. They need a mutable reference to be called.
FnOnce closures move something out of their captures, so they can only run once. After that the captured value is gone.
When you say impl Fn(...), you're telling the compiler the returned closure must satisfy the Fn trait. If the closure mutates state, you need impl FnMut. If it consumes a captured value, impl FnOnce.
This comes up the moment you try to make a counter:
// FnMut, because the closure mutates `count` each call.
fn make_counter() -> impl FnMut() -> u32 {
let mut count = 0;
move || {
count += 1;
count
}
}
fn main() {
let mut tick = make_counter();
println!("{}", tick()); // 1
println!("{}", tick()); // 2
println!("{}", tick()); // 3
}
Note let mut tick. Calling an FnMut closure mutates it, so the binding has to be mutable.
If you accidentally wrote impl Fn() -> u32 instead, you'd get:
error[E0594]: cannot assign to `count`, as it is a captured variable in a `Fn` closure
The fix is to widen the return type to FnMut.
Returning different closures from different branches
impl Trait has one important limitation: the function must return one concrete closure type. Because each closure literal is its own type, you can't do this:
fn pick_op(positive: bool) -> impl Fn(i32) -> i32 {
if positive {
|x| x + 1 // closure type A
} else {
|x| x - 1 // closure type B -- different from A!
}
}
Compiler:
error[E0308]: `if` and `else` have incompatible types
The two arms produce different (anonymous) types. impl Fn requires you pick one. The fix is to fall back on the heap-allocating version: a trait object inside a Box.
// `Box<dyn Fn...>` is a heap-allocated trait object.
// `dyn` says "any type implementing this trait". Different arms can return
// different concrete types, all hidden behind the same Box.
fn pick_op(positive: bool) -> Box<dyn Fn(i32) -> i32> {
if positive {
Box::new(|x| x + 1)
} else {
Box::new(|x| x - 1)
}
}
The trade-off: Box<dyn Fn...> adds an allocation and an indirect call. For most uses the cost is invisible. For tight loops it's worth measuring. Stick with impl Fn when you can; fall back to Box<dyn Fn> when you can't.
A more realistic example: a configurable filter
Here's a useful pattern: build a predicate based on a runtime config.
// Returns a closure that decides whether a log line passes the filter.
// `keyword` is captured by value so the predicate works after make_filter returns.
fn make_filter(keyword: String, min_len: usize) -> impl Fn(&str) -> bool {
move |line| line.len() >= min_len && line.contains(&keyword)
}
fn main() {
// Build a filter from config (could come from CLI args, env, file, ...).
let filter = make_filter(String::from("error"), 10);
let log = [
"warn: low disk",
"error: connection refused",
"info: started",
"err",
];
// Closures slot directly into iterator adapters.
for line in log.iter().filter(|l| filter(l)) {
println!("{line}");
}
}
The whole point: make_filter packages up the configuration once, hands you back a clean Fn(&str) -> bool you can pass anywhere. The capture is invisible to the caller. That's the API you want.
Lifetimes when the closure borrows
If the closure must borrow something instead of owning it, you'll need a lifetime annotation.
// The closure borrows `prefix`, so the closure can't outlive that borrow.
// The lifetime parameter `'a` ties them together.
fn make_prefixer<'a>(prefix: &'a str) -> impl Fn(&str) -> String + 'a {
move |s| format!("{}{}", prefix, s)
}
The + 'a says "the returned closure cannot outlive 'a." It's a tax you pay for borrowing instead of owning. Often easier to just take String and move it in.
Common pitfalls
Forgetting move when the closure outlives its capture. Compiler error E0373. Add move.
Returning bare Fn(...) instead of impl Fn(...) or Box<dyn Fn(...)>. The compiler will complain about an unsized return type.
Mixing closure types across if/else branches with impl Trait. Switch to Box<dyn Fn>.
Using Fn when you really need FnMut (mutation) or FnOnce (consumption). The borrow checker will tell you which one you actually need.
When to reach for what
Prefer impl Fn(...) when there's exactly one closure shape and you want zero allocation overhead.
Reach for Box<dyn Fn(...)> when the function might return one of several closures, or when storing many heterogeneous closures in a Vec.
If the closure mutates, return impl FnMut(...). If it consumes, impl FnOnce(...).
If you only call the closure inline in the same function, you don't need to return one at all. Just write the code.
Where to go next
Closures show up everywhere in Rust, and they're tied closely to iterators and ownership.
How to Define and Use Closures in Rust