๐Ÿฆ€ Functional Rust
๐ŸŽฌ Closures in Rust Fn/FnMut/FnOnce, capturing environment, move closures, higher-order functions.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Closures capture variables from their environment โ€” by reference, mutable reference, or by value (move)

โ€ข Three traits: Fn (shared borrow), FnMut (mutable borrow), FnOnce (takes ownership)

โ€ข Higher-order functions like .map(), .filter(), .fold() accept closures as arguments

โ€ข move closures take ownership of captured variables โ€” essential for threading

โ€ข Closures enable functional patterns: partial application, composition, and strategy

121: Closure Capture Modes

Difficulty: 2 Level: Intermediate Rust closures capture the minimum needed โ€” by reference, by mutable reference, or by ownership โ€” and you control which with the `move` keyword.

The Problem This Solves

When a closure refers to variables from its enclosing scope, those variables have to come from somewhere. In OCaml, the GC handles this transparently โ€” closures capture a reference to the environment, and the GC keeps it alive as long as needed. In Rust there's no GC, so the compiler must decide: does the closure borrow the variable, or does it own it? The compiler chooses the least restrictive mode: shared reference for read-only access, mutable reference if the closure mutates the variable. This is good โ€” but it has a limit. If the closure needs to outlive its creation scope (e.g., returned from a function or sent to a thread), it can't hold borrows into a stack frame that's about to disappear. The compiler will refuse. The fix is the `move` keyword. `move || ...` forces all captured variables to be moved into the closure, giving it ownership. The closure is now self-contained and can live as long as needed. For `Copy` types like `i32`, the "move" is just a copy, so the original variable is still usable.

The Intuition

By default closures borrow; add `move` when the closure needs to own its captures โ€” typically when returning a closure from a function or sending it to another thread.

How It Works in Rust

// By shared reference โ€” default for read-only access
let x = 42;
let f = || x + 1;   // borrows &x
assert_eq!(f(), 43);
assert_eq!(x, 42);  // x is still usable โ€” just borrowed

// By mutable reference โ€” default when closure mutates
let mut total = 0;
let mut add = |n: i32| total += n;  // borrows &mut total
add(10);
add(20);
drop(add);          // mutable borrow ends when closure is dropped
assert_eq!(total, 30);

// By move โ€” needed when closure outlives the creator
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
 move |x| x * factor
 // Without `move`, `factor` is borrowed from make_multiplier's stack.
 // With `move`, factor is owned by the closure โ€” safe to return.
}
let double = make_multiplier(2);
let triple = make_multiplier(3);
assert_eq!(double(5), 10);
assert_eq!(triple(5), 15);

// Move with non-Copy: original variable is gone
let name = String::from("Alice");
let greet = move || format!("Hello, {}!", name);
// name is moved โ€” this would fail: println!("{}", name);
println!("{}", greet());  // "Hello, Alice!"

What This Unlocks

Key Differences

ConceptOCamlRust
Capture modeAlways by GC reference โ€” implicitBy ref (default) or owned (`move`)
Outliving the scopeAlways works โ€” GC handles itRequires `move` (owned closure)
Copy types with `move`N/ACopied, not moved โ€” original still valid
Mutable capturesVia `ref` cell inside closureMutable borrow (`&mut`) โ€” one at a time
// Example 121: Closure Capture Modes
//
// Rust closures capture variables in three ways:
// 1. By shared reference (&T) โ€” default for read-only
// 2. By mutable reference (&mut T) โ€” when mutating
// 3. By move (T) โ€” with `move` keyword or when consuming

// Approach 1: Capture by shared reference
fn approach1() {
    let x = 42;
    let f = || x + 1; // captures &x
    assert_eq!(f(), 43);
    assert_eq!(x, 42); // x still accessible
    println!("f() = {}, x = {}", f(), x);
}

// Approach 2: Capture by mutable reference
fn approach2() {
    let mut total = 0;
    {
        let mut add = |x: i32| total += x; // captures &mut total
        add(10);
        add(20);
        add(30);
    } // mutable borrow ends here
    assert_eq!(total, 60);
    println!("Total: {}", total);
}

// Approach 3: Capture by move
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
    move |x| x * factor // `move` takes ownership of factor
    // Without `move`, factor would be borrowed โ€” but it would
    // be dropped when make_multiplier returns!
}

fn approach3() {
    let double = make_multiplier(2);
    let triple = make_multiplier(3);
    assert_eq!(double(5), 10);
    assert_eq!(triple(5), 15);
    println!("double(5)={}, triple(5)={}", double(5), triple(5));
    
    // Move with non-Copy types
    let name = String::from("Alice");
    let greet = move || format!("Hello, {}!", name);
    // name is moved โ€” can't use it here
    println!("{}", greet());
}

fn main() {
    println!("=== Approach 1: By Shared Reference ===");
    approach1();
    println!("\n=== Approach 2: By Mutable Reference ===");
    approach2();
    println!("\n=== Approach 3: By Move ===");
    approach3();
}

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

    #[test]
    fn test_capture_by_ref() {
        let v = vec![1, 2, 3];
        let len = || v.len(); // borrows v
        assert_eq!(len(), 3);
        assert_eq!(v, vec![1, 2, 3]); // v still accessible
    }

    #[test]
    fn test_capture_by_mut_ref() {
        let mut count = 0;
        let mut inc = || count += 1;
        inc();
        inc();
        drop(inc);
        assert_eq!(count, 2);
    }

    #[test]
    fn test_capture_by_move() {
        let s = String::from("hello");
        let f = move || s.len();
        assert_eq!(f(), 5);
        // s is moved โ€” would fail: assert_eq!(s, "hello");
    }

    #[test]
    fn test_move_copy_type() {
        let x = 42;
        let f = move || x; // moves, but i32 is Copy so x is still valid
        assert_eq!(f(), 42);
        assert_eq!(x, 42); // still works! i32 is Copy
    }

    #[test]
    fn test_returned_closure() {
        let f = make_multiplier(10);
        assert_eq!(f(3), 30);
        assert_eq!(f(7), 70);
    }
}
(* Example 121: Closure Capture Modes *)

(* OCaml closures always capture by reference (GC-managed).
   Rust closures capture by reference, mutable reference, or move. *)

(* Approach 1: Capture by reference (immutable) *)
let approach1 () =
  let x = 42 in
  let f = fun () -> x + 1 in  (* captures x by reference *)
  assert (f () = 43);
  assert (x = 42);  (* x unchanged *)
  Printf.printf "f() = %d, x = %d\n" (f ()) x

(* Approach 2: Capture mutable state *)
let approach2 () =
  let total = ref 0 in
  let add x = total := !total + x in
  add 10; add 20; add 30;
  assert (!total = 60);
  Printf.printf "Total: %d\n" !total

(* Approach 3: Closure outliving local scope *)
let make_multiplier factor =
  fun x -> x * factor  (* factor captured, GC keeps it alive *)

let approach3 () =
  let double = make_multiplier 2 in
  let triple = make_multiplier 3 in
  assert (double 5 = 10);
  assert (triple 5 = 15);
  Printf.printf "double(5)=%d, triple(5)=%d\n" (double 5) (triple 5)

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Comparison: Closure Capture Modes

Read-Only Capture

OCaml:

๐Ÿช Show OCaml equivalent
let x = 42 in
let f = fun () -> x + 1 in  (* GC reference *)
f ()

Rust:

let x = 42;
let f = || x + 1;  // captures &x
f()

Mutable Capture

OCaml:

๐Ÿช Show OCaml equivalent
let total = ref 0 in
let add x = total := !total + x in
add 10; add 20

Rust:

let mut total = 0;
let mut add = |x: i32| total += x;  // captures &mut total
add(10); add(20);

Move Capture (Returning Closures)

OCaml:

๐Ÿช Show OCaml equivalent
let make_multiplier factor =
fun x -> x * factor  (* GC keeps factor alive *)

Rust:

fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
 move |x| x * factor  // must move โ€” factor would be dropped otherwise
}