Closure Capture Rules
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
move to force ownership transfer into the closuremove closures over Copy types copy the value (not truly "move")impl Fn() -> T with movemut at the call siteCode 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
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
}
}#[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
Vec<String> and returns a thread::JoinHandle<usize> that sums the string lengths — requiring move to transfer ownership into the thread closure.drop(f).Fn (immutable borrow), then modify it to FnMut (add mutation), then FnOnce (consume the captured value) — observe how each change affects call-site requirements.