ExamplesBy LevelBy TopicLearning Paths
506 Intermediate

Closure Move Semantics

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Closure Move Semantics" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. When a closure is sent to another thread or returned from a function, it must own its captured data. Key difference from OCaml: 1. **Explicit vs. implicit move**: Rust requires `move` to be explicit; OCaml always captures by GC reference (conceptually always "moved to the heap").

Tutorial

The Problem

When a closure is sent to another thread or returned from a function, it must own its captured data. The enclosing scope (and its stack frame) will be gone when the closure executes. move || data.len() moves data into the closure's environment at creation time. Without move, the borrow checker would reject the code because the borrowed reference would dangle. This is why thread::spawn requires move closures: the spawned thread may outlive the spawning thread's stack frame.

🎯 Learning Outcomes

  • • Use move to transfer ownership of captured values into a closure
  • • Understand that move with Copy types copies the value (semantically the same as moving)
  • • Return a move closure that outlives its creating scope
  • • Clone a value before moving to retain a copy in the original scope
  • • Recognise move as mandatory for thread::spawn and async blocks
  • Code Example

    #![allow(clippy::all)]
    //! # Closure Move Semantics — Ownership Transfer
    
    use std::thread;
    
    /// Move closure for threads
    pub fn spawn_with_data(data: Vec<i32>) -> thread::JoinHandle<i32> {
        thread::spawn(move || {
            data.iter().sum() // data moved into closure
        })
    }
    
    /// Move individual values
    pub fn move_multiple() -> impl FnOnce() -> (String, Vec<i32>) {
        let s = String::from("hello");
        let v = vec![1, 2, 3];
    
        move || (s, v) // Both moved
    }
    
    /// Partial move
    pub fn partial_move() {
        let data = (String::from("hello"), 42);
    
        let f = move || {
            let (s, n) = data; // Takes ownership of both
            println!("{} {}", s, n);
        };
    
        f();
    }
    
    /// Clone before move
    pub fn clone_then_move(s: String) -> (impl Fn() -> usize, String) {
        let cloned = s.clone();
        let f = move || cloned.len();
        (f, s) // Return both the closure and original
    }
    
    /// Force move with move keyword
    pub fn force_move() -> impl Fn() -> i32 {
        let x = 42;
        move || x // x is Copy, but move forces ownership transfer semantics
    }
    
    /// Move into async block (conceptual)
    pub fn move_for_async_like() -> impl FnOnce() -> String {
        let data = String::from("async data");
        move || {
            // Simulates async - data must be owned
            data
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_spawn_with_data() {
            let data = vec![1, 2, 3, 4, 5];
            let handle = spawn_with_data(data);
            assert_eq!(handle.join().unwrap(), 15);
        }
    
        #[test]
        fn test_move_multiple() {
            let f = move_multiple();
            let (s, v) = f();
            assert_eq!(s, "hello");
            assert_eq!(v, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_clone_then_move() {
            let s = String::from("test");
            let (f, original) = clone_then_move(s);
            assert_eq!(f(), 4);
            assert_eq!(original, "test");
        }
    
        #[test]
        fn test_force_move() {
            let f = force_move();
            assert_eq!(f(), 42);
            assert_eq!(f(), 42); // Can still call because i32 is Copy
        }
    
        #[test]
        fn test_async_like() {
            let f = move_for_async_like();
            assert_eq!(f(), "async data");
        }
    }

    Key Differences

  • Explicit vs. implicit move: Rust requires move to be explicit; OCaml always captures by GC reference (conceptually always "moved to the heap").
  • **'static bound on threads**: Rust's thread::spawn requires F: 'static + Sendmove ensures 'static; OCaml has no such bound.
  • Clone discipline: Rust's clone_then_move requires explicit .clone() to retain both a closure and the original; OCaml captures by shared reference automatically.
  • **FnOnce enforcement**: A move closure that consumes a non-Copy value can only be called once (FnOnce); OCaml has no type-level equivalent.
  • OCaml Approach

    OCaml closures automatically capture variables by reference to GC-managed values — the GC prevents dangling:

    let spawn_with_data data =
      let result = ref 0 in
      Domain.spawn (fun () -> result := List.fold_left (+) 0 data);
      result  (* data captured by reference; GC keeps it alive *)
    

    In Multicore OCaml, domains capture the enclosing value by reference; the GC ensures safety. There is no move keyword because values never "move" — they stay on the heap and the GC manages their lifetime.

    Full Source

    #![allow(clippy::all)]
    //! # Closure Move Semantics — Ownership Transfer
    
    use std::thread;
    
    /// Move closure for threads
    pub fn spawn_with_data(data: Vec<i32>) -> thread::JoinHandle<i32> {
        thread::spawn(move || {
            data.iter().sum() // data moved into closure
        })
    }
    
    /// Move individual values
    pub fn move_multiple() -> impl FnOnce() -> (String, Vec<i32>) {
        let s = String::from("hello");
        let v = vec![1, 2, 3];
    
        move || (s, v) // Both moved
    }
    
    /// Partial move
    pub fn partial_move() {
        let data = (String::from("hello"), 42);
    
        let f = move || {
            let (s, n) = data; // Takes ownership of both
            println!("{} {}", s, n);
        };
    
        f();
    }
    
    /// Clone before move
    pub fn clone_then_move(s: String) -> (impl Fn() -> usize, String) {
        let cloned = s.clone();
        let f = move || cloned.len();
        (f, s) // Return both the closure and original
    }
    
    /// Force move with move keyword
    pub fn force_move() -> impl Fn() -> i32 {
        let x = 42;
        move || x // x is Copy, but move forces ownership transfer semantics
    }
    
    /// Move into async block (conceptual)
    pub fn move_for_async_like() -> impl FnOnce() -> String {
        let data = String::from("async data");
        move || {
            // Simulates async - data must be owned
            data
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_spawn_with_data() {
            let data = vec![1, 2, 3, 4, 5];
            let handle = spawn_with_data(data);
            assert_eq!(handle.join().unwrap(), 15);
        }
    
        #[test]
        fn test_move_multiple() {
            let f = move_multiple();
            let (s, v) = f();
            assert_eq!(s, "hello");
            assert_eq!(v, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_clone_then_move() {
            let s = String::from("test");
            let (f, original) = clone_then_move(s);
            assert_eq!(f(), 4);
            assert_eq!(original, "test");
        }
    
        #[test]
        fn test_force_move() {
            let f = force_move();
            assert_eq!(f(), 42);
            assert_eq!(f(), 42); // Can still call because i32 is Copy
        }
    
        #[test]
        fn test_async_like() {
            let f = move_for_async_like();
            assert_eq!(f(), "async data");
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_spawn_with_data() {
            let data = vec![1, 2, 3, 4, 5];
            let handle = spawn_with_data(data);
            assert_eq!(handle.join().unwrap(), 15);
        }
    
        #[test]
        fn test_move_multiple() {
            let f = move_multiple();
            let (s, v) = f();
            assert_eq!(s, "hello");
            assert_eq!(v, vec![1, 2, 3]);
        }
    
        #[test]
        fn test_clone_then_move() {
            let s = String::from("test");
            let (f, original) = clone_then_move(s);
            assert_eq!(f(), 4);
            assert_eq!(original, "test");
        }
    
        #[test]
        fn test_force_move() {
            let f = force_move();
            assert_eq!(f(), 42);
            assert_eq!(f(), 42); // Can still call because i32 is Copy
        }
    
        #[test]
        fn test_async_like() {
            let f = move_for_async_like();
            assert_eq!(f(), "async data");
        }
    }

    Deep Comparison

    Closure Move Semantics: Comparison

    See src/lib.rs for the Rust implementation.

    Exercises

  • Parallel map: Write fn parallel_map<T: Send + 'static, U: Send + 'static>(data: Vec<T>, f: impl Fn(T) -> U + Send + Sync + 'static) -> Vec<U> using thread::scope or Arc + move closures.
  • Move vs. clone benchmark: Create a Vec<String> with 1000 elements and measure the time of thread::spawn(move || ...) vs. cloning the vec before spawn using criterion.
  • Async simulation: Write fn make_async_task(data: String) -> impl FnOnce() -> String returning a move closure that simulates a deferred computation — verify it can be called from a different scope.
  • Open Source Repos