Rust Patterns That Matter #16: Storing and Returning Closures
Post 16 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #15: Fn, FnMut, FnOnce | Next: #17: Builder Pattern
You want a struct with a callback field. What type do you give it? You want a function that returns a closure. What return type do you write? The answer touches one of Rust's most distinctive features: every closure has a unique, anonymous type that you can't name.
The motivation
struct Button {
label: String,
on_click: ???, // what type goes here?
}
You try fn(), but that only works for function pointers, not closures
that capture variables. You try Fn():
error[E0277]: the size for values of type `dyn Fn()`
cannot be known at compile time
dyn Fn() is a trait object - it has no fixed size. You can't
put an unsized type directly in a struct field.
Why closure types are anonymous
Each closure the compiler generates is a unique struct. It captures the
variables it needs and implements the appropriate Fn trait. The struct's
name is generated by the compiler and never exposed to you.
let a = |x: i32| x + 1;
let b = |x: i32| x + 2;
// a and b have DIFFERENT types, even though both are Fn(i32) -> i32
This is intentional. Unique types let the compiler monomorphise: each closure's call
is inlined directly, with no dynamic dispatch overhead. But it means you can't write
a struct field like callback: the_type_of_my_closure because that type
has no name.
Pattern 1: Box<dyn Fn()>
Heap-allocate the closure behind a trait object. The struct stores a fixed-size pointer:
struct Button {
label: String,
on_click: Box<dyn Fn()>,
}
impl Button {
fn new(label: impl Into<String>, on_click: impl Fn() + 'static) -> Self {
Button {
label: label.into(),
on_click: Box::new(on_click),
}
}
fn click(&self) {
(self.on_click)();
}
}
fn main() {
let btn = Button::new("Submit", || println!("clicked!"));
btn.click();
}
Box<dyn Fn()> is the pragmatic default for stored closures.
One heap allocation to store the closure, and a vtable pointer for dynamic dispatch.
The overhead is negligible for event handlers and callbacks.
Pattern 2: Generics
If you want to avoid the heap allocation, make the struct generic over the closure type:
struct Button<F: Fn()> {
label: String,
on_click: F,
}
This is monomorphised: no heap allocation, no dynamic dispatch. The closure is stored inline. But the struct's type now includes the closure's anonymous type, which means:
- You can't store different
Buttons in the sameVec(they have different types) - The type parameter propagates everywhere the
Buttonis used
Generics work well when a struct has exactly one callback and you don't need to
store heterogeneous instances. For anything more flexible, use Box<dyn Fn>.
Returning closures
To return a closure from a function, use impl Fn in the return type:
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let add_five = make_adder(5);
println!("{}", add_five(3)); // 8
impl Fn(i32) -> i32 means "I'm returning some concrete type that
implements this trait, but I'm not telling you which one." The compiler knows the
concrete type and monomorphises the caller. No heap allocation.
The limitation: all return paths must return the same concrete type. You can't conditionally return different closures:
// ERROR: `if` and `else` have incompatible types
fn make_op(add: bool) -> impl Fn(i32) -> i32 {
if add {
|x| x + 1
} else {
|x| x * 2 // different anonymous type!
}
}
Boxing for conditional returns
When you need to return different closures from different branches, box them:
fn make_op(add: bool) -> Box<dyn Fn(i32) -> i32> {
if add {
Box::new(|x| x + 1)
} else {
Box::new(|x| x * 2)
}
}
Both branches return Box<dyn Fn(i32) -> i32> - a single
concrete type (the box) wrapping different trait objects. This compiles.
Telex's approach: Rc<dyn Fn()>
Telex stores callbacks as Rc<dyn Fn()> instead of
Box<dyn Fn()>. The reason: a single callback might be cloned
into multiple event handlers (a button's click handler registered with both a
keyboard shortcut and a mouse target). Rc allows sharing the same
closure without heap-allocating separate copies. See
Designing a TUI Framework -
Part 1 for details.
When to use which
Box<dyn Fn>- struct fields, heterogeneous collections, any time you need to store a closure and the type can vary- Generics (
F: Fn) - hot paths where you want inlining, or when the struct has a single closure that's known at construction time impl Fn- return types when there's one concrete closure being returnedRc<dyn Fn>- when the same closure needs to be shared (cloned) across multiple ownersfn()pointer - when the closure captures nothing (function pointers are simpler and have no allocation)
See it in practice: Building a Chat Server #4: Commands and Plugins uses this pattern for the filter registry.
Telex