Closures in Rust
Closures are anonymous functions that can capture values from their surrounding scope. They're similar to lambdas in other languages.
Basic Closure Syntax
fn main() {
// Basic closure
let add_one = |x| x + 1;
let result = add_one(5);
println!("Result: {}", result); // 6
}
Type Inference
Closures can infer parameter and return types:
fn main() {
// Type inference
let add = |x, y| x + y;
let sum = add(5, 3);
println!("Sum: {}", sum); // 8
// Explicit types
let multiply = |x: i32, y: i32| -> i32 { x * y };
println!("Product: {}", multiply(4, 5)); // 20
}
Capturing Variables
Closures can capture variables from their environment:
fn main() {
let multiplier = 3;
let multiply_by_three = |x| x * multiplier;
println!("5 × 3 = {}", multiply_by_three(5)); // 15
}
Capture Modes
Closures capture variables in three ways:
1. By Immutable Reference
fn main() {
let message = String::from("Hello");
let print_message = || println!("{}", message);
print_message();
print_message();
// message is still accessible
println!("Original: {}", message);
}
2. By Mutable Reference
fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
println!("Count: {}", count);
};
increment(); // Count: 1
increment(); // Count: 2
}
3. By Value (Move)
fn main() {
let name = String::from("Alice");
let consume_name = move || {
println!("Name: {}", name);
};
consume_name();
// name is no longer accessible here
// println!("{}", name); // Error!
}
Function Traits
Closures implement one or more of these traits:
Fn - Immutable Borrow
fn apply_twice<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(f(x))
}
fn main() {
let double = |x| x * 2;
let result = apply_twice(double, 5);
println!("Result: {}", result); // 20
}
FnMut - Mutable Borrow
fn apply_to_vec<F>(vec: &mut Vec<i32>, mut f: F)
where
F: FnMut(&mut i32),
{
for item in vec {
f(item);
}
}
fn main() {
let mut numbers = vec![1, 2, 3];
apply_to_vec(&mut numbers, |x| *x *= 2);
println!("Doubled: {:?}", numbers); // [2, 4, 6]
}
FnOnce - Takes Ownership
fn consume_with<F>(f: F)
where
F: FnOnce() -> String,
{
println!("Result: {}", f());
}
fn main() {
let msg = String::from("Hello");
consume_with(move || msg);
// msg is no longer available
}
Closures as Parameters
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Using closures with iterator methods
let squares: Vec<i32> = numbers.iter()
.map(|&x| x * x)
.collect();
println!("Squares: {:?}", squares);
let evens: Vec<&i32> = numbers.iter()
.filter(|&&x| x % 2 == 0)
.collect();
println!("Evens: {:?}", evens);
}
Returning Closures
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
move |y| x + y
}
fn main() {
let add_5 = make_adder(5);
println!("10 + 5 = {}", add_5(10)); // 15
let add_10 = make_adder(10);
println!("20 + 10 = {}", add_10(20)); // 30
}
Complex Examples
Closure Factory
fn create_calculator(op: char) -> Box<dyn Fn(i32, i32) -> i32> {
match op {
'+' => Box::new(|a, b| a + b),
'-' => Box::new(|a, b| a - b),
'*' => Box::new(|a, b| a * b),
'/' => Box::new(|a, b| a / b),
_ => Box::new(|_, _| 0),
}
}
fn main() {
let add = create_calculator('+');
let multiply = create_calculator('*');
println!("5 + 3 = {}", add(5, 3));
println!("5 * 3 = {}", multiply(5, 3));
}
Memoization with Closures
use std::collections::HashMap;
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
values: HashMap<u32, u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.values.get(&arg) {
Some(&v) => v,
None => {
let v = (self.calculation)(arg);
self.values.insert(arg, v);
v
}
}
}
}
fn main() {
let mut expensive_closure = Cacher::new(|num| {
println!("Calculating slowly...");
std::thread::sleep(std::time::Duration::from_secs(1));
num * 2
});
println!("First call: {}", expensive_closure.value(5));
println!("Second call: {}", expensive_closure.value(5)); // Cached!
}
Event Handler Pattern
struct Button {
click_handlers: Vec<Box<dyn Fn()>>,
}
impl Button {
fn new() -> Self {
Button {
click_handlers: Vec::new(),
}
}
fn on_click<F>(&mut self, handler: F)
where
F: Fn() + 'static,
{
self.click_handlers.push(Box::new(handler));
}
fn click(&self) {
for handler in &self.click_handlers {
handler();
}
}
}
fn main() {
let mut button = Button::new();
button.on_click(|| println!("Button clicked!"));
button.on_click(|| println!("Another handler!"));
button.click();
}
Practical Use Cases
Sorting with Custom Comparator
fn main() {
let mut people = vec![
("Alice", 30),
("Bob", 25),
("Charlie", 35),
];
// Sort by age
people.sort_by(|a, b| a.1.cmp(&b.1));
println!("By age: {:?}", people);
// Sort by name
people.sort_by(|a, b| a.0.cmp(&b.0));
println!("By name: {:?}", people);
}
Lazy Evaluation
fn main() {
let expensive_calculation = || {
println!("Performing expensive calculation...");
std::thread::sleep(std::time::Duration::from_secs(1));
42
};
let use_result = false;
if use_result {
let result = expensive_calculation();
println!("Result: {}", result);
} else {
println!("Skipped calculation");
}
}
Best Practices
- Use closures for short, simple functions
- Prefer
movewhen the closure outlives the current scope - Use explicit types when clarity is needed
- Consider performance implications of capturing large values
- Use function traits (
Fn,FnMut,FnOnce) to be explicit about closure requirements