19 - Closures
📋 Jump to Takeaways🎁 You write a tiny anonymous function, and it just grabs variables from the surrounding code without you passing them in. No special syntax to "close over" the environment, no explicit capture lists. The compiler figures out what to borrow, what to mutate, and what to move, all from how you use the variables inside.
Closure Syntax
A closure is an anonymous function you can store in a variable or pass as an argument. You write parameters between pipes instead of parentheses.
fn main() {
let add_one = |x| x + 1;
let multiply = |x, y| x * y;
println!("{}", add_one(5)); // 6
println!("{}", multiply(3, 4)); // 12
// Multi-line closures use braces
let calculate = |x: i32| {
let doubled = x * 2;
doubled + 10
};
println!("{}", calculate(5)); // 20
}Regular functions require explicit type annotations — closures don't. The compiler infers parameter and return types from how the closure is used:
fn double_fn(x: i32) -> i32 { x * 2 } // function: must annotate
let double_cl = |x| x * 2;
double_cl(5); // closure: compiler sees i32, infers the restOnce inferred, the types are locked in. You can't call the same closure with different types:
let identity = |x| x;
identity(5); // locks in x: i32
identity("hello"); // ❌ ERROR — already inferred as i32Closures are not generic. If you need something that works with multiple types, use a generic function instead:
fn identity<T>(x: T) -> T { x }
println!("{}", identity(5)); // ✅ i32
println!("{}", identity("hello")); // ✅ &strCapturing Variables
Closures can capture variables from their surrounding scope. This is what makes them more powerful than plain functions.
fn main() {
let name = String::from("Rust");
let greeting = || println!("Hello, {}!", name); // borrows name
greeting(); // Hello, Rust!
println!("{}", name); // still valid — closure only borrowed it
}Rust determines how a closure captures based on what the closure body does. This maps to three traits:
Fnborrows captured values immutably. Can be called many times.FnMutborrows captured values mutably. Can be called many times but needsmutaccess.FnOncetakes ownership of captured values. Can only be called once.
Rust picks the least restrictive trait automatically. If the closure only reads a variable, it's Fn. If it mutates, it's FnMut. If it moves the value out, it's FnOnce.
fn main() {
let name = String::from("Alice");
// Fn — only reads `name`, borrows it immutably
let greet = || println!("Hello, {}", name);
greet();
greet(); // ✅ can call multiple times
println!("{}", name); // ✅ `name` still usable
let mut count = 0;
// FnMut — modifies `count`, borrows it mutably
let mut increment = || { count += 1; };
increment();
increment(); // ✅ can call multiple times
println!("count: {}", count); // count: 2
let message = String::from("goodbye");
// FnOnce — moves `message` into the closure body
let consume = || {
let m = message;
println!("{}", m);
};
consume(); // goodbye
// consume(); // ❌ ERROR: can't call again, `message` was moved
}The move Keyword
Sometimes you need the closure to own the captured data, especially when sending it to another thread. Use move before the pipes.
fn main() {
let data = vec![1, 2, 3];
let owns_data = move || {
println!("{:?}", data);
};
owns_data(); // [1, 2, 3]
// println!("{:?}", data); // ❌ ERROR: data was moved into the closure
}With move, all captured variables are moved into the closure regardless of how the body uses them.
Functions That Accept Functions
In Go you pass a function by writing the signature inline: func apply(f func(int) int, x int) int. In Rust you use the Fn traits as bounds.
fn apply(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
let double = |n| n * 2;
let square = |n| n * n;
println!("{}", apply(double, 5)); // 10
println!("{}", apply(square, 5)); // 25
}impl Fn(i32) -> i32 means "anything callable that takes an i32 and returns an i32". This works for closures and regular functions.
You can also pass a named function directly:
fn add_one(n: i32) -> i32 {
n + 1
}
fn apply_twice(f: impl Fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn main() {
println!("{}", apply_twice(add_one, 5)); // 7
}Which Fn trait to use as the bound depends on what the caller might pass:
| Bound | What the function can do with it | Use when |
|---|---|---|
impl Fn() |
Call it many times, read-only | Most cases |
impl FnMut() |
Call it many times, might mutate | Accumulating results |
impl FnOnce() |
Call it exactly once | Consuming a value |
Pick the most permissive bound that works. FnOnce accepts all closures. Fn is the most restrictive (but most callers prefer it because they can call repeatedly).
fn run_twice(f: impl Fn()) {
f();
f();
}
fn run_once(f: impl FnOnce()) {
f();
// f(); // ❌ ERROR: can't call FnOnce twice
}
fn main() {
let greeting = String::from("hello");
run_twice(|| println!("{}", greeting)); // ✅ Fn, borrows greeting
let name = String::from("world");
run_once(move || println!("{}", name)); // ✅ FnOnce, moves name
}There's also the bare fn pointer type for when you don't need closures at all:
fn apply_fn_ptr(f: fn(i32) -> i32, x: i32) -> i32 {
f(x)
}
fn main() {
println!("{}", apply_fn_ptr(|n| n + 1, 5)); // 6
}fn(i32) -> i32 (lowercase) is a function pointer. It works for closures that capture nothing and for named functions. It cannot accept closures that capture variables. Use impl Fn when you want to accept closures too.
Key Takeaways
- Closures use
|params| bodysyntax and infer types from context Fnborrows immutably,FnMutborrows mutably,FnOnceconsumes captured valuesmoveforces a closure to take ownership of all captured variables- Accept closures as parameters with
impl Fn(Args) -> Return fn(Args) -> Return(lowercase) is for function pointers only, no captures allowed- Fn is the most restrictive trait (read-only); FnOnce is the most permissive (accepts everything)
- Once a closure's types are inferred, they're locked in for all subsequent calls — use a generic function if you need multiple types
🎁 You've seen closures passed to functions. But what about chaining them? Write .map().filter().collect() and it reads like English, yet compiles to the same machine code as a hand-written loop. That's the iterator system, and it changes how you think about data processing.