๐Ÿฆ€ Functional Rust
๐ŸŽฌ Rust Ownership in 30 seconds Visual walkthrough of ownership, moves, and automatic memory management.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Each value in Rust has exactly one owner โ€” when the owner goes out of scope, the value is dropped

โ€ข Assignment moves ownership by default; the original binding becomes invalid

โ€ข Borrowing (&T / &mut T) lets you reference data without taking ownership

โ€ข The compiler enforces: many shared references OR one mutable reference, never both

โ€ข No garbage collector needed โ€” memory is freed deterministically at scope exit

528: Closures Capturing References

Difficulty: 4 Level: Intermediate-Advanced When closures borrow instead of own, their lifetimes are constrained โ€” understanding this explains the most confusing closure errors.

The Problem This Solves

You write `let checker = make_prefix_checker(&prefix)` and then try to store `checker` in a struct or return it โ€” only to get lifetime errors: `checker does not live long enough` or `borrowed value must be valid for the lifetime 'a`. You add lifetime annotations and it still doesn't work. The root cause: a closure that captures `&T` borrows the owner for the closure's entire lifetime. The closure is the borrow. You can't use the original variable while the closure exists (for mutable captures), and you can't let the closure outlive the variable. These errors protect you from use-after-free bugs. But without understanding the mechanics, they feel arbitrary and the solutions seem like guesswork.

The Intuition

A closure capturing a reference is like a sticky note attached to a book. As long as the sticky note exists, the book must exist too. You can't throw out the book while the note is still attached to it. In Python and JavaScript, the GC handles this automatically โ€” the closure holds a reference that keeps the object alive. Rust's borrow checker does the same job at compile time, without GC overhead: if the closure exists, the borrowed data must also exist. The fix depends on what you need:

How It Works in Rust

// Closure captures &str โ€” its lifetime is tied to prefix
fn make_prefix_checker<'a>(prefix: &'a str) -> impl Fn(&str) -> bool + 'a {
 //                 ^^                                                 ^^
 //   lifetime of the borrow                    closure inherits this lifetime
 move |s| s.starts_with(prefix)   // prefix is a &str reference โ€” owned by string
}

let prefix = String::from("hello");
let checker = make_prefix_checker(&prefix);   // checker borrows prefix
println!("{}", checker("hello world"));        // โœ“

// Correct order: drop checker BEFORE dropping prefix
drop(checker);   // borrow released
drop(prefix);    // now safe to drop

// Struct holding a closure that borrows โ€” struct needs lifetime parameter
struct Filter<'a, T> {
 data: &'a [T],
 predicate: Box<dyn Fn(&T) -> bool + 'a>,   // 'a: predicate can't outlive data
}
impl<'a, T> Filter<'a, T> {
 fn new(data: &'a [T], pred: impl Fn(&T) -> bool + 'a) -> Self {
     Filter { data, predicate: Box::new(pred) }
 }
 fn apply(&self) -> Vec<&T> {
     self.data.iter().filter(|x| (self.predicate)(x)).collect()
 }
}

let numbers = vec![1, 2, 3, 4, 5, 6];
let threshold = 3;
// threshold is captured by reference โ€” Filter lifetime tied to threshold
let filter = Filter::new(&numbers, move |&x| x > threshold);
println!("{:?}", filter.apply()); // [4, 5, 6]

// Multiple shared borrows are fine โ€” closures only read
let data = vec![1, 2, 3, 4, 5];
let sum_closure = || data.iter().sum::<i32>();   // shared borrow of data
let max_closure = || data.iter().max().copied(); // another shared borrow โ€” fine
println!("{} {:?}", sum_closure(), max_closure()); // both can coexist

// This WOULD NOT compile:
// let checker2;
// { let local = String::from("temp");
//   checker2 = make_prefix_checker(&local); }  // local dropped here
// checker2("test"); // โœ— use after free โ€” caught at compile time

What This Unlocks

Key Differences

ConceptOCamlRust
Reference captureImplicit โ€” GC keeps referent aliveInferred lifetime borrow โ€” must not outlive referent
Closure lifetimeGC handles โ€” can store anywhereCannot outlive the borrowed values
Return borrowing closureNo issue โ€” GCRequires explicit `+ 'a` lifetime annotation
Mutable reference captureVia `ref`Exclusive borrow โ€” no other access while closure exists
Fix: outlive scopeN/AUse `move` to transfer ownership instead
//! # 528. Closures Capturing References
//! How closure lifetimes are constrained by captured borrows.

/// Closure captures &str โ€” lifetime tied to the string's scope
fn make_prefix_checker<'a>(prefix: &'a str) -> impl Fn(&str) -> bool + 'a {
    move |s| s.starts_with(prefix)
}

/// Multiple borrows in a closure
fn make_range_checker<'a>(data: &'a [i32]) -> impl Fn(i32) -> bool + 'a {
    move |target| data.contains(&target)
}

/// Struct holding a closure that borrows
struct Filter<'a, T> {
    data: &'a [T],
    predicate: Box<dyn Fn(&T) -> bool + 'a>,
}

impl<'a, T> Filter<'a, T> {
    fn new(data: &'a [T], pred: impl Fn(&T) -> bool + 'a) -> Self {
        Filter { data, predicate: Box::new(pred) }
    }

    fn apply(&self) -> Vec<&T> {
        self.data.iter().filter(|x| (self.predicate)(x)).collect()
    }
}

fn main() {
    // Closure borrows prefix โ€” closure lifetime <= prefix's lifetime
    let prefix = String::from("hello");
    let checker = make_prefix_checker(&prefix);
    println!("'hello world' starts with 'hello': {}", checker("hello world"));
    println!("'hi there' starts with 'hello': {}", checker("hi there"));
    drop(checker); // drop closure before dropping prefix
    drop(prefix);  // now safe to drop prefix

    // Range checker borrowing a slice
    let allowed = vec![1, 3, 5, 7, 9];
    let is_allowed = make_range_checker(&allowed);
    for n in 0..=10 {
        if is_allowed(n) { print!("{} ", n); }
    }
    println!("are allowed");

    // Filter struct borrowing its data
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let threshold = 5;
    let filter = Filter::new(&numbers, move |&x| x > threshold);
    let result = filter.apply();
    println!("numbers > {}: {:?}", threshold, result);

    // Named lifetime prevents use-after-free
    // This WON'T compile โ€” closure outlives borrow:
    // let checker2;
    // {
    //     let local = String::from("temp");
    //     checker2 = make_prefix_checker(&local); // ERROR
    // }
    // checker2("test"); // local is gone!

    // Shared borrow โ€” multiple closures can borrow the same data
    let data = vec![1, 2, 3, 4, 5];
    let sum_closure = || data.iter().sum::<i32>();
    let max_closure = || data.iter().max().copied();
    println!("sum: {}, max: {:?}", sum_closure(), max_closure());
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_prefix_checker() {
        let prefix = String::from("rust");
        let check = make_prefix_checker(&prefix);
        assert!(check("rustacean"));
        assert!(!check("python"));
    }

    #[test]
    fn test_range_checker() {
        let allowed = vec![2, 4, 6, 8];
        let check = make_range_checker(&allowed);
        assert!(check(4));
        assert!(!check(5));
    }

    #[test]
    fn test_filter_struct() {
        let data = vec![1, 2, 3, 4, 5];
        let f = Filter::new(&data, |&x| x % 2 == 0);
        let evens = f.apply();
        assert_eq!(evens, vec![&2, &4]);
    }
}
(* Closures capturing references in OCaml โ€” GC manages lifetimes *)
let make_length_checker s =
  (* s is captured by GC reference โ€” lives as long as closure *)
  fun () -> String.length s

let () =
  let len =
    let s = "hello world" in
    make_length_checker s  (* s kept alive by closure *)
  in
  Printf.printf "length: %d\n" (len ())