iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Closures

A Rust closure is an anonymous function that can capture variables from its enclosing scope. Three flavours by capture mode: Fn (borrows), FnMut (mutably borrows), FnOnce (takes ownership).

Fn, FnMut, FnOnce, move, iterators

EXAMPLE
// 1) Basic closure syntax
let square = |x: i32| x * x;
println!("{}", square(5));                    // 25

// Type inference — explicit annotations optional
let sum = |a, b| a + b;
let result = sum(3, 4);                       // i32, inferred

// Multi-line body — use braces
let sum_with_log = |a: i32, b: i32| -> i32 {
    println!("adding {} + {}", a, b);
    a + b
};

// 2) Capturing variables
let x = 10;
let add_x = |n| n + x;                        // captures x by reference
add_x(5);                                      // 15

let mut count = 0;
let mut increment = || count += 1;             // captures &mut
increment();
increment();
println!("{}", count);                         // 2

// 3) move keyword — take ownership
let name = String::from("Ada");
let greet = move || println!("Hi, {}", name);  // name MOVED into closure
// name no longer usable here
greet();

// Useful for threads (closure outlives the original scope)
use std::thread;
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
    println!("{:?}", data);                    // ownership moved into thread
});
handle.join().unwrap();

// 4) Closure traits — Fn, FnMut, FnOnce

// Fn: takes &self → can be called multiple times, doesn't mutate captures
fn call_n_times<F: Fn()>(f: F, n: u32) {
    for _ in 0..n {
        f();
    }
}

let greet = || println!("hello");
call_n_times(greet, 3);

// FnMut: takes &mut self → can mutate captures, multiple calls OK
fn run_mut<F: FnMut()>(mut f: F) {
    f();
    f();
}

let mut counter = 0;
run_mut(|| counter += 1);
println!("{}", counter);

// FnOnce: takes self → can be called AT MOST ONCE (consumes captures)
fn run_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

let name = String::from("Ada");
let greet = move || name;                      // moves name; can only call once
run_once(greet);

// 5) Closures as iterator combinators
let nums = vec![1, 2, 3, 4, 5];

let doubled: Vec<i32> = nums.iter().map(|n| n * 2).collect();
let evens:   Vec<i32> = nums.iter().filter(|&&n| n % 2 == 0).copied().collect();
let sum:     i32     = nums.iter().sum();
let max:     Option<i32> = nums.iter().copied().max();
let found:   Option<&i32> = nums.iter().find(|&&n| n > 3);

// 6) Higher-order — return closures from functions
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y                             // move x into the closure
}

let add_5 = make_adder(5);
println!("{}", add_5(10));                     // 15

// 7) Box<dyn Fn> — when impl Fn isn't enough (e.g. branching)
fn pick(op: &str) -> Box<dyn Fn(i32, i32) -> i32> {
    match op {
        "add" => Box::new(|a, b| a + b),
        "sub" => Box::new(|a, b| a - b),
        _     => Box::new(|a, b| 0),
    }
}

let f = pick("add");
println!("{}", f(3, 4));                       // 7

// 8) Storing closures in structs
struct Worker {
    on_done: Box<dyn Fn(String)>,
}

impl Worker {
    fn process(&self, data: String) {
        // ... work ...
        (self.on_done)(data);
    }
}

let w = Worker {
    on_done: Box::new(|s| println!("done: {}", s)),
};
w.process("hello".to_string());

// 9) Capture-by-reference vs by-move (decided automatically)
let data = vec![1, 2, 3];
let print_len = || println!("len: {}", data.len());     // captures &data
print_len();
print_len();                                              // still works
println!("{:?}", data);                                   // still usable

let take = move || println!("len: {}", data.len());     // captures data by value
take();
// data is no longer accessible

// 10) Sharing state with Arc + Mutex
use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
    let counter = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut num = counter.lock().unwrap();
        *num += 1;
    }));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap());     // 10

// 11) Closures + lifetimes
fn run<'a, F: Fn(&'a str) -> &'a str>(input: &'a str, f: F) -> &'a str {
    f(input)
}

let upper = |s: &str| s;
run("hello", upper);

// 12) Async closures (stable in Rust 1.85)
async fn run<F, Fut>(f: F) -> i32
where
    F: FnOnce() -> Fut,
    Fut: std::future::Future<Output = i32>,
{
    f().await
}

// Or use the explicit async block:
let f = || async { 42 };
run(f).await;

// 13) Function pointers vs closures
fn double(x: i32) -> i32 { x * 2 }

let f: fn(i32) -> i32 = double;                // fn — function pointer
let g = |x| x * 2;                              // closure — can capture

// Function pointers can be passed where Fn is expected
let doubled: Vec<i32> = vec![1, 2, 3].iter().map(|x| double(*x)).collect();

// 14) Patterns in callbacks
fn for_each<T, F: FnMut(&T)>(items: &[T], mut f: F) {
    for item in items {
        f(item);
    }
}

for_each(&[1, 2, 3], |x| println!("{}", x));

let mut sum = 0;
for_each(&[1, 2, 3], |x| sum += x);             // FnMut — captures sum
println!("{}", sum);                            // 6

// 15) Common patterns from the standard library

// thread::spawn
std::thread::spawn(move || {
    // body
});

// std::sync::Once
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
    // init code
});

// std::iter::repeat_with
std::iter::repeat_with(|| rand::random::<u32>())
    .take(5)
    .collect::<Vec<_>>();

// Option::map / Option::unwrap_or_else
let x: Option<i32> = Some(5);
let doubled = x.map(|n| n * 2);
let default = x.unwrap_or_else(|| compute_default());

// 16) Common bugs
//   • Borrow checker errors → use move or restructure
//   • Closure outlives borrowed value → use move or Arc
//   • Calling FnOnce twice → compile error
//   • Returning closure that captures local → use impl Fn or Box<dyn Fn>
//   • Shared mutable state → use Mutex/RwLock; raw &mut won't work across closures

// 17) Performance
// - Closures monomorphise — zero overhead vs hand-written struct + method
// - Box<dyn Fn> has one vtable indirection (~1ns)
// - impl Trait return — preferred for single concrete type, avoids the Box
// - Inline closures in iterator chains are optimised aggressively

// 18) Best practices
//   ✅ Let the compiler infer capture mode (it picks the most restrictive)
//   ✅ Use move only when you need to (clarifies intent)
//   ✅ Prefer impl Fn over Box<dyn Fn> when types unify
//   ✅ Pair with iterators for declarative pipelines
//   ✅ For shared mutable state, use Arc<Mutex<T>>
//   ✅ For async, use async move || { ... } closures

Why it matters

Closures + iterators give Rust its zero-cost expressive power. Let the compiler pick capture mode (Fn/FnMut/FnOnce); reach for move when sending closures to threads, and Arc<Mutex<T>> for shared mutable state.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
let add = |a, b| a + b;
println!("{}", add(2, 3));

let n = 10;
let add_n = move |x| x + n;
println!("{}", add_n(5));
Try it Yourself »

Discussion

Loading…