🎒 Rust Closures: Your Pocket-Sized Helper Functions
The Story of the Magic Backpack
Imagine you have a magic backpack. This backpack is special because:
- You can put instructions inside it
- It can grab things from your room and carry them along
- You can give it to someone else, and they can follow the instructions using what’s inside
That’s exactly what a closure is in Rust!
A closure is a tiny function you can:
- Write quickly (short syntax)
- Store in a variable
- Pass around like a toy
- It can “grab” things from its surroundings
1. Closure Syntax: Writing Your First Magic Note
Regular functions in Rust look like this:
fn add_one(x: i32) -> i32 {
x + 1
}
But closures are shorter and sweeter:
let add_one = |x| x + 1;
The Parts of a Closure
let greet = |name| {
println!("Hello, {}!", name);
};
| Part | What It Means |
|---|---|
|name| |
The input (like a door for things to enter) |
{ ... } |
The instructions inside |
let greet = |
Give the closure a name |
Even Simpler!
If your closure has just ONE expression, skip the curly braces:
let double = |x| x * 2;
Example: Using a Closure
let square = |n| n * n;
println!("{}", square(5)); // Prints: 25
2. Closure Type Inference: Rust Figures It Out!
Here’s something magical: you don’t always need to tell Rust the types!
Rust looks at how you USE the closure and figures out the types automatically.
let add = |a, b| a + b;
// Rust sees you're adding numbers
let result = add(3, 4); // result = 7
But Here’s the Rule
Once Rust decides the types, they’re locked in:
let echo = |x| x;
let s = echo("hello"); // x is now &str
// let n = echo(5); // ERROR! Can't use i32
Think of it like this: The first time you put something in a box, the box takes that shape forever.
When You WANT to Specify Types
let multiply: fn(i32, i32) -> i32 = |a, b| {
a * b
};
Or inside the closure:
let divide = |a: f64, b: f64| -> f64 {
a / b
};
3. Capturing Environment: The Backpack Grabs Things!
This is where closures become really special.
Closures can reach outside and grab variables from their surroundings:
let name = String::from("Alice");
let greet = || {
println!("Hello, {}!", name);
};
greet(); // Prints: Hello, Alice!
The closure “grabbed” the name variable!
How Does Rust Grab Things?
Rust has three ways to grab things:
graph TD A["Closure Captures Variable"] --> B{How?} B --> C["📖 Borrow<br>Just look at it"] B --> D["✏️ Mutable Borrow<br>Look and change it"] B --> E["📦 Take Ownership<br>Take it completely"]
Example: Borrowing (just looking)
let message = String::from("Hi!");
let print_it = || {
println!("{}", message); // Just reading
};
print_it();
println!("{}", message); // Still works!
Example: Mutable Borrow (changing)
let mut count = 0;
let mut increment = || {
count += 1; // Changing the value
};
increment();
increment();
println!("{}", count); // Prints: 2
4. The Fn Trait: “I Promise to Only Look!”
When a closure only reads from its environment (doesn’t change anything), it implements the Fn trait.
let name = String::from("Bob");
let say_hi = || {
println!("Hi, {}!", name);
};
// This closure implements Fn
// It only READS name, never changes it
Why Does This Matter?
You can call Fn closures as many times as you want:
fn call_twice<F: Fn()>(f: F) {
f();
f();
}
let greeting = || println!("Hello!");
call_twice(greeting); // Works perfectly!
Think of Fn like a library book: You can read it over and over, but you can’t write in it.
5. The FnMut Trait: “I Might Change Things!”
When a closure modifies something it captured, it implements FnMut.
let mut total = 0;
let mut add_to_total = |x| {
total += x; // Changing total!
};
add_to_total(5);
add_to_total(10);
println!("{}", total); // Prints: 15
The Rules
- The closure must be declared
mut - You can still call it multiple times
- But only ONE mutable reference at a time!
fn call_with_one<F: FnMut(i32)>(mut f: F) {
f(1);
}
let mut sum = 0;
call_with_one(|x| sum += x);
Think of FnMut like a notebook: You can write in it, erase, and write again!
6. The FnOnce Trait: “One-Time Magic!”
Some closures can only be called once because they consume what they captured.
let name = String::from("Charlie");
let consume_name = || {
drop(name); // name is gone forever!
};
consume_name();
// consume_name(); // ERROR! Can't call again
When Does FnOnce Happen?
When your closure moves something out:
let data = vec![1, 2, 3];
let consume_data = || {
let _moved = data; // data moves INTO closure
println!("Data consumed!");
};
consume_data(); // Works once
// consume_data(); // ERROR!
The Trait Hierarchy
graph TD A["FnOnce"] --> B["FnMut"] B --> C["Fn"] style A fill:#ffcccc style B fill:#ffffcc style C fill:#ccffcc
Every Fn is also FnMut and FnOnce!
| Trait | Can Call | Modifies? | Consumes? |
|---|---|---|---|
Fn |
Many times | No | No |
FnMut |
Many times | Yes | No |
FnOnce |
Once | Maybe | Yes |
7. Move Closures: “Take Everything With Me!”
Sometimes you WANT the closure to own its captured values, even if it doesn’t need to.
Use the move keyword:
let name = String::from("Dana");
let greet = move || {
println!("Hello, {}!", name);
};
greet();
// println!("{}", name); // ERROR! name was moved
Why Use Move?
Perfect for threads! When you spawn a new thread, it needs to OWN its data:
use std::thread;
let message = String::from("Hi from thread!");
let handle = thread::spawn(move || {
println!("{}", message);
});
handle.join().unwrap();
Without move, the thread might outlive the data!
Move + Copy Types
For types that implement Copy (like numbers), move makes a copy:
let x = 42;
let print_x = move || {
println!("{}", x);
};
print_x();
println!("{}", x); // Still works! x was copied
🎯 Putting It All Together
fn main() {
// 1. Simple closure syntax
let add = |a, b| a + b;
// 2. Type inference works!
let result = add(2, 3); // Rust knows: i32
// 3. Capturing environment
let multiplier = 10;
let multiply = |x| x * multiplier;
// 4. Fn - just reading
println!("{}", multiply(5)); // 50
// 5. FnMut - changing things
let mut count = 0;
let mut counter = || { count += 1; count };
println!("{}", counter()); // 1
println!("{}", counter()); // 2
// 6. FnOnce - consuming
let data = vec![1, 2, 3];
let consume = || drop(data);
consume();
// 7. Move closure
let name = String::from("Rust");
let greet = move || println!("Hello, {}!", name);
greet();
}
🚀 Quick Reference
| Concept | What It Does | Example |
|---|---|---|
| Basic Syntax | |params| body |
|x| x + 1 |
| Type Inference | Rust figures out types | |a, b| a + b |
| Environment Capture | Grabs nearby variables | || println!("{}", name) |
Fn |
Read-only access | Can call many times |
FnMut |
Can modify captured vars | Needs mut |
FnOnce |
Consumes captured vars | Call once only |
move |
Takes ownership | move || ... |
🌟 Remember This!
Closures are like magic backpacks:
- Quick to make (short syntax)
- Can grab things from nearby (capture)
- Can be passed around (first-class functions)
- Rust decides how they grab things (Fn, FnMut, FnOnce)
- Use
movewhen you want them to OWN everything inside
You’ve got this! Closures might seem tricky at first, but they’re just tiny, portable functions with superpowers. 🎒✨
