ExamplesBy LevelBy TopicLearning Paths
501 Intermediate

Closure Capture Rules

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Capture Rules" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. In languages with garbage collection, closures capture variables freely — the GC prevents dangling references. Key difference from OCaml: 1. **Automatic capture mode**: Rust infers the minimum capture mode; OCaml always captures by reference (GC handles lifetimes).

Tutorial

The Problem

In languages with garbage collection, closures capture variables freely — the GC prevents dangling references. Rust has no GC, so the compiler must prove that closure captures are safe. It enforces that: (1) if the closure only reads, it borrows immutably; (2) if it mutates, it borrows mutably (preventing other borrows); (3) if it needs to outlive the captured variable (e.g., return from a function or sent to a thread), it must take ownership via move. These rules are the closure-specific application of the borrow checker.

🎯 Learning Outcomes

  • • Understand that closures capture by immutable borrow, mutable borrow, or move automatically
  • • Use move to force ownership transfer into the closure
  • • Know that move closures over Copy types copy the value (not truly "move")
  • • Return a closure from a function using impl Fn() -> T with move
  • • Understand why mutable borrow closures must be declared mut at the call site
  • Code Example

    #![allow(clippy::all)]
    //! # Closure Capture Rules — Borrowing and Moving
    
    pub fn capture_by_ref() {
        let s = String::from("hello");
        let f = || println!("{}", s); // Borrows s
        f();
        f();
        println!("{}", s); // s still valid
    }
    
    pub fn capture_by_mut_ref() {
        let mut v = vec![1, 2, 3];
        let mut f = || v.push(4); // Mutably borrows v
        f();
        // Can't use v here until f is dropped
        drop(f);
        println!("{:?}", v);
    }
    
    pub fn capture_by_move() {
        let s = String::from("hello");
        let f = move || println!("{}", s); // Moves s into closure
        f();
        // s is no longer valid here
    }
    
    pub fn different_captures() -> impl Fn() {
        let a = 42; // Copy type
        let b = String::from("hello"); // Move type
    
        move || {
            println!("{} {}", a, b); // a copied, b moved
        }
    }
    
    pub fn return_closure_capturing_param(x: i32) -> impl Fn() -> i32 {
        move || x * 2
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_captures() {
            capture_by_ref();
            capture_by_mut_ref();
            capture_by_move();
        }
    
        #[test]
        fn test_return_closure() {
            let f = return_closure_capturing_param(21);
            assert_eq!(f(), 42);
        }
    
        #[test]
        fn test_closure_copies_copy_types() {
            let x = 42;
            let f = move || x;
            assert_eq!(f(), 42);
            assert_eq!(x, 42); // x still valid because i32 is Copy
        }
    }

    Key Differences

  • Automatic capture mode: Rust infers the minimum capture mode; OCaml always captures by reference (GC handles lifetimes).
  • **mut requirement**: Rust requires mut f at the declaration site for mutable-capturing closures; OCaml has no such requirement.
  • **move keyword**: Rust's move forces ownership transfer for cross-function or cross-thread use; OCaml has no equivalent because GC manages all lifetimes.
  • **Copy types with move**: Rust copies Copy types into move closures rather than moving; OCaml always copies primitive values into closures by value.
  • OCaml Approach

    OCaml closures always capture by reference to the GC-managed heap — ownership is irrelevant:

    let s = "hello"
    let f () = Printf.printf "%s\n" s  (* captures reference *)
    let () = f (); f (); Printf.printf "%s\n" s  (* all valid *)
    
    (* Mutable state — use ref *)
    let v = ref [1;2;3]
    let push x () = v := x :: !v
    let () = push 4 (); Printf.printf "%d\n" (List.length !v)
    

    Full Source

    #![allow(clippy::all)]
    //! # Closure Capture Rules — Borrowing and Moving
    
    pub fn capture_by_ref() {
        let s = String::from("hello");
        let f = || println!("{}", s); // Borrows s
        f();
        f();
        println!("{}", s); // s still valid
    }
    
    pub fn capture_by_mut_ref() {
        let mut v = vec![1, 2, 3];
        let mut f = || v.push(4); // Mutably borrows v
        f();
        // Can't use v here until f is dropped
        drop(f);
        println!("{:?}", v);
    }
    
    pub fn capture_by_move() {
        let s = String::from("hello");
        let f = move || println!("{}", s); // Moves s into closure
        f();
        // s is no longer valid here
    }
    
    pub fn different_captures() -> impl Fn() {
        let a = 42; // Copy type
        let b = String::from("hello"); // Move type
    
        move || {
            println!("{} {}", a, b); // a copied, b moved
        }
    }
    
    pub fn return_closure_capturing_param(x: i32) -> impl Fn() -> i32 {
        move || x * 2
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_captures() {
            capture_by_ref();
            capture_by_mut_ref();
            capture_by_move();
        }
    
        #[test]
        fn test_return_closure() {
            let f = return_closure_capturing_param(21);
            assert_eq!(f(), 42);
        }
    
        #[test]
        fn test_closure_copies_copy_types() {
            let x = 42;
            let f = move || x;
            assert_eq!(f(), 42);
            assert_eq!(x, 42); // x still valid because i32 is Copy
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_captures() {
            capture_by_ref();
            capture_by_mut_ref();
            capture_by_move();
        }
    
        #[test]
        fn test_return_closure() {
            let f = return_closure_capturing_param(21);
            assert_eq!(f(), 42);
        }
    
        #[test]
        fn test_closure_copies_copy_types() {
            let x = 42;
            let f = move || x;
            assert_eq!(f(), 42);
            assert_eq!(x, 42); // x still valid because i32 is Copy
        }
    }

    Deep Comparison

    Closure Capture Rules: Comparison

    See src/lib.rs for the Rust implementation.

    Exercises

  • Thread closure: Write a function that takes a Vec<String> and returns a thread::JoinHandle<usize> that sums the string lengths — requiring move to transfer ownership into the thread closure.
  • Borrow conflict: Write code that demonstrates the compiler error when trying to use a mutable-borrow closure alongside another borrow of the same variable, then fix it with drop(f).
  • Closure upgrade: Write a closure that starts as Fn (immutable borrow), then modify it to FnMut (add mutation), then FnOnce (consume the captured value) — observe how each change affects call-site requirements.
  • Open Source Repos