Writing a closure's type as the return type seems natural once you've seen |x| x + 1 work everywhere else — but Fn(i32) -> i32 is a trait, not a concrete type, and a function's return type has to be one concrete, sized thing the compiler can lay out on the stack. Every closure gets its own hidden, compiler-generated type, so the compiler can't just hand back "a closure" without saying how.
fn make_adder(x: i32) -> Fn(i32) -> i32 {
move |y| x + y
}
let add_five = make_adder(5);
println!("{}", add_five(10));
// error[E0746]: return type cannot be a trait object without pointer indirection
// help: consider returning an `impl Trait` instead of a `dyn Trait`A closure is written with pipes around its parameters (|x| x + 1) and can capture variables from its surrounding scope. The compiler infers which of three traits it implements based on how it uses those captures: FnOnce (consumes a captured value, callable once), FnMut (mutably borrows a capture, callable multiple times), or Fn (only reads captures, callable multiple times, even concurrently). The move keyword forces a closure to take ownership of its captures rather than borrowing them.
Closure syntax and type inference
Closures rarely need type annotations — the compiler infers parameter and return types from how the closure is used, unlike a function definition, which always requires them.
let add_one = |x: i32| x + 1; // explicit types, optional
let multiply = |x, y| x * y; // types inferred from first call
println!("{}", add_one(5)); // 6
println!("{}", multiply(3, 4)); // 12, infers i32 from these argsType inference also explains the fix for the bug above: the compiler already knows the concrete closure type at every call site, so a return type only needs to describe which trait that hidden type implements, not the type itself. Writing impl Fn(i32) -> i32 instead of a bare Fn(i32) -> i32 says exactly that — "some concrete type implementing this trait" — which is sized and returnable, without naming or boxing it:
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
let add_five = make_adder(5);
println!("{}", add_five(10)); // 15Capturing the environment: Fn, FnMut, FnOnce
A closure that only reads a captured variable implements Fn; one that mutates a captured variable implements FnMut; one that moves a captured variable out (consuming it) implements FnOnce and can only be called once.
let list = vec![1, 2, 3];
let print_list = || println!("{:?}", list); // Fn: only reads list
print_list();
print_list(); // fine, can call again — only borrowed immutably
let mut count = 0;
let mut increment = || { count += 1; }; // FnMut: mutates count
increment();
increment();
println!("{}", count); // 2move: taking ownership of captures
The move keyword forces a closure to take ownership of everything it captures, rather than borrowing — essential when the closure needs to outlive the current scope, such as when it's sent to a new thread.
let data = vec![1, 2, 3];
let handle = std::thread::spawn(move || { // move: data's ownership transfers in
println!("{:?}", data);
});
// println!("{:?}", data); // COMPILE ERROR: data was moved into the closure
handle.join().unwrap();