ExamplesBy LevelBy TopicLearning Paths
121 Intermediate

Closure Capture Modes

Functional Programming

Tutorial

The Problem

When a closure uses a variable from its enclosing scope, it must determine how to capture it: share a reference, take a mutable reference, or move ownership. Getting this wrong leads to use-after-free (in unsafe code), data races, or borrow conflicts. Rust enforces the correct capture mode at compile time based on how the closure uses each variable. Understanding capture modes is essential for writing closures that outlive their creation scope (e.g., closures passed to threads).

🎯 Learning Outcomes

  • • Understand the three capture modes: by shared reference, by mutable reference, and by move
  • • Learn when the move keyword is required and what it implies for Copy vs non-Copy types
  • • See how the borrow checker prevents concurrent mutation through a captured &mut T
  • • Practice building closures that are safe to return from functions or send across threads
  • Code Example

    // Closure returned from a function must own its captures — use `move`
    pub fn add_one_to(x: i32) -> impl Fn() -> i32 {
        move || x + 1   // `i32` is Copy, so this copies x into the closure
    }
    
    pub fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
        move |x| x * factor
    }
    
    pub fn make_greeter(name: String) -> impl Fn() -> String {
        move || format!("Hello, {}!", name)  // String is moved into the closure
    }

    Key Differences

  • Capture mechanism: OCaml always captures by environment reference (GC-managed); Rust chooses the minimal capture mode or moves on move.
  • Move semantics: Rust's move closures take ownership — the original binding is no longer usable; OCaml has no equivalent concept.
  • Mutable capture: OCaml requires ref cells for mutable captures; Rust infers &mut capture automatically when the closure mutates a variable.
  • Thread safety: Rust move closures over Send types can be sent to threads; OCaml closures are not thread-safe by default (Domain API is separate).
  • OCaml Approach

    OCaml closures always capture variables by reference to a shared, GC-managed environment. There is no move keyword — the GC keeps captured values alive as long as the closure lives. Mutation inside OCaml closures requires explicit ref cells; a plain let x = 5 captured in a closure is read-only by structural convention, not enforced by the type system.

    Full Source

    #![allow(clippy::all)]
    //! Example 121: Closure Capture Modes
    //!
    //! Rust closures capture variables from their enclosing scope in three ways:
    //! 1. By shared reference (`&T`) — default when only reading
    //! 2. By mutable reference (`&mut T`) — when the closure mutates the variable
    //! 3. By move (`T`) — with the `move` keyword; the closure owns its captures
    //!
    //! OCaml closures always capture by reference (GC-managed environment).
    //! Rust requires you to be explicit when ownership is needed.
    
    // ── Approach 1: Capture by shared reference (immutable borrow) ───────────────
    //
    // The compiler infers `&x` because the closure only reads `x`.
    // After the closure is created, `x` is still fully accessible.
    pub fn add_one_to(x: i32) -> impl Fn() -> i32 {
        // `move` is required here so the closure can outlive this stack frame.
        // For `Copy` types like `i32`, moving is equivalent to copying — `x`
        // would still be usable after the closure is created if we were in the
        // same scope. Here we need `move` to return the closure.
        move || x + 1
    }
    
    // ── Approach 2: Capture by mutable reference ─────────────────────────────────
    //
    // The closure takes `&mut total` because it writes to `total`.
    // Only one `&mut` borrow can exist at a time, so we scope it tightly.
    pub fn sum_with_closure(values: &[i32]) -> i32 {
        let mut total = 0;
        {
            // `add` borrows `total` mutably; the borrow ends at the end of this block.
            let mut add = |x: i32| total += x;
            for &v in values {
                add(v);
            }
        } // mutable borrow released here — `total` is readable again
        total
    }
    
    // ── Approach 3: move closure — closure owns its captures ─────────────────────
    //
    // `make_multiplier` returns a closure. The closure must own `factor` because
    // the stack frame that created `factor` is gone by the time the closure runs.
    // `move` gives the closure ownership; for `Copy` types this is just a copy.
    pub fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
        move |x| x * factor
    }
    
    // ── Approach 4: move closure capturing a non-Copy value ──────────────────────
    //
    // When the captured value is not `Copy` (e.g. `String`), `move` transfers
    // ownership into the closure. The original binding is no longer accessible.
    pub fn make_greeter(name: String) -> impl Fn() -> String {
        move || format!("Hello, {}!", name)
    }
    
    // ── Approach 5: functional accumulator returning a new closure ────────────────
    //
    // Demonstrates that closures are first-class values: we build a pipeline of
    // closures, each moving its own state.
    pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
        move |x| x + n
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_shared_borrow_via_move_copy() {
            // For Copy types, `move` copies the value; original is unaffected in
            // the same scope. Here we verify the returned closure computes correctly.
            let f = add_one_to(42);
            assert_eq!(f(), 43);
            // Calling multiple times — closure is Fn, not FnOnce.
            assert_eq!(f(), 43);
        }
    
        #[test]
        fn test_mutable_capture_accumulates() {
            let result = sum_with_closure(&[10, 20, 30]);
            assert_eq!(result, 60);
    
            let empty = sum_with_closure(&[]);
            assert_eq!(empty, 0);
        }
    
        #[test]
        fn test_move_closure_make_multiplier() {
            let double = make_multiplier(2);
            let triple = make_multiplier(3);
            assert_eq!(double(5), 10);
            assert_eq!(triple(5), 15);
            // Both closures are independent — they own separate copies of `factor`.
            assert_eq!(double(0), 0);
        }
    
        #[test]
        fn test_move_closure_non_copy_string() {
            let greet = make_greeter("Alice".to_string());
            assert_eq!(greet(), "Hello, Alice!");
            // Closure is `Fn` — can be called multiple times.
            assert_eq!(greet(), "Hello, Alice!");
        }
    
        #[test]
        fn test_adder_closure_pipeline() {
            let add5 = make_adder(5);
            let add10 = make_adder(10);
            // Each closure owns its own `n`.
            assert_eq!(add5(1), 6);
            assert_eq!(add10(1), 11);
            // Compose by chaining calls.
            assert_eq!(add10(add5(0)), 15);
        }
    
        #[test]
        fn test_closure_captures_independently() {
            // Two closures from the same factory do not share state.
            let a = make_multiplier(7);
            let b = make_multiplier(7);
            assert_eq!(a(3), b(3));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_shared_borrow_via_move_copy() {
            // For Copy types, `move` copies the value; original is unaffected in
            // the same scope. Here we verify the returned closure computes correctly.
            let f = add_one_to(42);
            assert_eq!(f(), 43);
            // Calling multiple times — closure is Fn, not FnOnce.
            assert_eq!(f(), 43);
        }
    
        #[test]
        fn test_mutable_capture_accumulates() {
            let result = sum_with_closure(&[10, 20, 30]);
            assert_eq!(result, 60);
    
            let empty = sum_with_closure(&[]);
            assert_eq!(empty, 0);
        }
    
        #[test]
        fn test_move_closure_make_multiplier() {
            let double = make_multiplier(2);
            let triple = make_multiplier(3);
            assert_eq!(double(5), 10);
            assert_eq!(triple(5), 15);
            // Both closures are independent — they own separate copies of `factor`.
            assert_eq!(double(0), 0);
        }
    
        #[test]
        fn test_move_closure_non_copy_string() {
            let greet = make_greeter("Alice".to_string());
            assert_eq!(greet(), "Hello, Alice!");
            // Closure is `Fn` — can be called multiple times.
            assert_eq!(greet(), "Hello, Alice!");
        }
    
        #[test]
        fn test_adder_closure_pipeline() {
            let add5 = make_adder(5);
            let add10 = make_adder(10);
            // Each closure owns its own `n`.
            assert_eq!(add5(1), 6);
            assert_eq!(add10(1), 11);
            // Compose by chaining calls.
            assert_eq!(add10(add5(0)), 15);
        }
    
        #[test]
        fn test_closure_captures_independently() {
            // Two closures from the same factory do not share state.
            let a = make_multiplier(7);
            let b = make_multiplier(7);
            assert_eq!(a(3), b(3));
        }
    }

    Deep Comparison

    OCaml vs Rust: Closure Capture Modes

    Side-by-Side Code

    OCaml

    (* OCaml closures always capture by reference — the GC keeps the environment alive *)
    
    (* Capture by value (immutable) *)
    let add_one_to x = fun () -> x + 1
    
    (* Mutable capture via ref cell *)
    let sum_with_closure values =
      let total = ref 0 in
      let add x = total := !total + x in
      List.iter add values;
      !total
    
    (* Closure outliving its creation scope — GC handles lifetime *)
    let make_multiplier factor = fun x -> x * factor
    
    (* Non-Copy capture — GC keeps string alive *)
    let make_greeter name = fun () -> "Hello, " ^ name ^ "!"
    

    Rust (idiomatic — move closures for owned captures)

    // Closure returned from a function must own its captures — use `move`
    pub fn add_one_to(x: i32) -> impl Fn() -> i32 {
        move || x + 1   // `i32` is Copy, so this copies x into the closure
    }
    
    pub fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
        move |x| x * factor
    }
    
    pub fn make_greeter(name: String) -> impl Fn() -> String {
        move || format!("Hello, {}!", name)  // String is moved into the closure
    }
    

    Rust (functional/recursive — mutable capture in local scope)

    pub fn sum_with_closure(values: &[i32]) -> i32 {
        let mut total = 0;
        // Compiler infers &mut total because the closure mutates it.
        // Borrow ends when the closure is dropped.
        let mut add = |x: i32| total += x;
        for &v in values { add(v); }
        drop(add);
        total
    }
    

    Type Signatures

    ConceptOCamlRust
    Returned closureval make_multiplier : int -> int -> intfn make_multiplier(factor: i32) -> impl Fn(i32) -> i32
    Mutable statelet r = ref 0let mut total = 0
    String ownershipGC reference (transparent)String moved with move \|\|
    Callable type'a -> 'b (uniform)Fn, FnMut, or FnOnce (distinct traits)

    Key Insights

  • GC vs ownership: OCaml's GC keeps captured environments alive automatically. Rust requires the programmer to decide — borrow or move — and enforces the decision at compile time.
  • Three capture modes: Rust closures capture by &T (read-only), &mut T (mutation in scope), or T (ownership via move). The compiler chooses the least restrictive mode; move overrides that choice.
  • **move for Copy types is free:** For types like i32 that implement Copy, move copies the value into the closure — the original variable is still accessible in the same scope. The effect is purely about letting the closure outlive its creation scope.
  • **move for non-Copy types transfers ownership:** For String or Vec, move actually moves the value into the closure. The original binding becomes inaccessible, matching what OCaml achieves transparently through the GC.
  • **Fn / FnMut / FnOnce:** Rust expresses capture semantics in the type system. A closure that only reads is Fn; one that mutates captures is FnMut; one that consumes captures is FnOnce. OCaml has no such distinction — all closures are uniform functions.
  • When to Use Each Style

    Use default (borrow) capture when: the closure lives entirely within the enclosing scope and only needs to read or mutate a local variable — the compiler handles it correctly and no annotation is needed.

    **Use move when:** the closure must outlive its creation scope (returned from a function, passed to a thread, stored in a struct). For Copy types this is a cheap copy; for heap types it transfers ownership into the closure.

    Exercises

  • Write a make_adder(n: i32) -> impl Fn(i32) -> i32 using move and verify the original n is still usable after the closure is created (because i32 is Copy).
  • Try capturing a String by shared reference in a returned closure — observe the lifetime error, then fix it with move.
  • Implement a thread-safe counter using Arc<Mutex<u32>> captured by a move closure passed to thread::spawn.
  • Open Source Repos