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 argsCapturing 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();