Working Code
Rust lets you create functions without names. These are called closures.
fn main() {
let add_one = |x: i32| x + 1;
println!("{}", add_one(5)); // 6
// Multi-line closures work too
let add_and_print = |a: i32, b: i32| -> i32 {
let sum = a + b;
println!("{a} + {b} = {sum}");
sum
};
add_and_print(3, 4); // 3 + 4 = 7
}
|parameters| body — that's the basic closure form. Compare it to a regular function:
// Regular function
fn add_one_fn(x: i32) -> i32 {
x + 1
}
// Closure
let add_one = |x: i32| x + 1;
Closures can also omit type annotations — the compiler infers them:
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}
Try It Yourself
- Create a closure
|x| x * xto compute squares. - Create a closure
|a, b| if a > b { a } else { b }that returns the larger of two values.
"Why?" — Closures Remember Their Surroundings
The key difference between closures and regular functions is environment capture. Closures can "grab" variables from the scope where they're defined:
fn main() {
let threshold = 10;
// The closure captures threshold from the outer scope!
let is_big = |x: i32| x > threshold;
println!("{}", is_big(5)); // false
println!("{}", is_big(15)); // true
}
Regular functions can't do this — you'd have to pass threshold explicitly as a parameter.
Three Capture Modes
How a closure captures variables determines its trait. This ties directly to the ownership rules!
| Capture mode | Trait | Analogy | Example |
| ---------------- | -------- | ----------------------- | ------- | --- | ------------------ |
| Immutable borrow | Fn | Just reading a book | | x | x + threshold |
| Mutable borrow | FnMut | Writing notes in a book | | x | { count += 1; x } |
| Takes ownership | FnOnce | Taking the book away | move | x | { drop(name); x } |
fn main() {
// Fn — captures by immutable reference
let name = String::from("Rust");
let greet = || println!("Hello, {name}!");
greet();
greet(); // can call multiple times
println!("{name}"); // name still usable
// FnMut — captures by mutable reference
let mut count = 0;
let mut increment = || {
count += 1;
println!("count: {count}");
};
increment(); // count: 1
increment(); // count: 2
// FnOnce — takes ownership
let message = String::from("bye");
let consume = move || {
println!("{message}");
// message's ownership moved into the closure
};
consume();
// println!("{message}"); // error! message was moved
}
Tip: In most cases, Rust decides the capture mode automatically. It tries
Fnfirst, thenFnMut, thenFnOnce— picking the most restrictive option that works. You rarely need to specify it yourself!
The move Keyword
Adding move forces the closure to take ownership of every captured variable:
fn main() {
let name = String::from("Alice");
// Without move: borrows name
let greet = || println!("Hello, {name}!");
greet();
println!("Still usable: {name}");
// With move: ownership transfers to the closure
let greet_move = move || println!("Hello, {name}!");
greet_move();
// println!("{name}"); // error! ownership moved
}
move is especially important when sending data to threads — a topic for later!
Passing Closures to Functions
You can pass closures as arguments using trait bounds:
fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
f(f(x))
}
fn main() {
let double = |x| x * 2;
let result = apply_twice(double, 3);
println!("{result}"); // 12 (3 -> 6 -> 12)
// Regular functions work too
fn add_ten(x: i32) -> i32 { x + 10 }
let result2 = apply_twice(add_ten, 5);
println!("{result2}"); // 25 (5 -> 15 -> 25)
}
The generics and trait bounds from Lesson 21 are at work here! F: Fn(i32) -> i32 means "a function (or closure) that takes an i32 and returns an i32."
Real-World Example: A Cacher
Combining closures with structs, you can build a caching (memoization) pattern:
struct Cacher<F: Fn(i32) -> i32> {
calculation: F,
value: Option<i32>,
}
impl<F: Fn(i32) -> i32> Cacher<F> {
fn new(calculation: F) -> Cacher<F> {
Cacher {
calculation,
value: None,
}
}
fn get(&mut self, arg: i32) -> i32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
fn main() {
let mut expensive = Cacher::new(|num| {
println!("Computing...");
num * 2
});
println!("Result: {}", expensive.get(5)); // "Computing..." printed
println!("Result: {}", expensive.get(5)); // cached value used, no print
}
Run an expensive computation once and cache the result!
Exercise 1. Complete the function. Apply a closure to every element of a vector and return a new vector.
fn transform<F: Fn(i32) -> i32>(numbers: &[i32], f: F) -> Vec<i32> {
// Complete this
// Hint: create an empty Vec and push f(n) in a for loop
}
fn main() {
let nums = vec![1, 2, 3, 4, 5];
let squared = transform(&nums, |x| x * x);
let tripled = transform(&nums, |x| x * 3);
println!("Squared: {:?}", squared); // [1, 4, 9, 16, 25]
println!("Tripled: {:?}", tripled); // [3, 6, 9, 12, 15]
}
Exercise 2. Complete the filter_by function. Return only elements that satisfy the predicate.
fn filter_by<F: Fn(&i32) -> bool>(numbers: &[i32], predicate: F) -> Vec<i32> {
// Complete this
}
fn main() {
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let evens = filter_by(&nums, |x| x % 2 == 0);
let big = filter_by(&nums, |x| *x > 5);
println!("Evens: {:?}", evens); // [2, 4, 6, 8, 10]
println!("> 5: {:?}", big); // [6, 7, 8, 9, 10]
}
Q1. What is the biggest difference between closures and regular functions?
- A) Closures can't return values
- B) Closures can capture variables from their surrounding environment
- C) Closures can't accept parameters
- D) Regular functions are faster
Q2. What is the capture mode of this closure?
let mut total = 0;
let mut add = |x: i32| { total += x; };
add(5);
- A)
Fn— captures by immutable reference - B)
FnMut— captures by mutable reference - C)
FnOnce— takes ownership - D) No capture
Q3. What happens when you put move before a closure?
- A) The closure runs faster
- B) Captured variables' ownership transfers to the closure
- C) The closure can't be called more than once
- D) The closure can't return values