326: Capturing with async move
Tutorial Video
Text description (accessibility)
This video demonstrates the "326: Capturing with async move" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Async tasks often need to use data from the surrounding scope — a user ID, a connection string, or a shared counter. Key difference from OCaml: 1. **Ownership transfer**: Rust's `move` explicitly transfers ownership to the closure — the original binding can no longer be used; OCaml closures share by reference with GC management.
Tutorial
The Problem
Async tasks often need to use data from the surrounding scope — a user ID, a connection string, or a shared counter. Since async tasks may outlive the scope where they are created, they cannot borrow — they must own their data. The async move { } block (and move || closure) captures all referenced variables by value, giving the async task ownership. This is required whenever a spawned task needs access to outer-scope data.
🎯 Learning Outcomes
move || and async move { } as capturing environment by ownership'static lifetime — they must own their dataArc<Mutex<T>>move captures (one task per capture) and Arc clones (multiple tasks sharing)Code Example
fn make_greeter(name: String) -> impl Fn() {
move || println!("Hello, {name}!")
}Key Differences
move explicitly transfers ownership to the closure — the original binding can no longer be used; OCaml closures share by reference with GC management.thread::spawn / tokio::spawn require 'static (owned) data; move is the primary tool to satisfy this.Arc::clone() before each move || gives each task its own reference-counted pointer.move || that captures an owned non-Clone value implements FnOnce — can only be called once; Arc enables Fn (callable many times).OCaml Approach
OCaml closures capture variables from the enclosing scope by reference (the GC handles lifetimes), so explicit move is not needed:
let make_greeter name = fun () -> "Hello, " ^ name ^ "!"
(* `name` is captured by the closure — GC ensures it lives long enough *)
For Lwt concurrent tasks, Lwt.async with shared mutable refs:
let counter = ref 0
let increment () = Lwt.return (incr counter; !counter)
Full Source
#![allow(clippy::all)]
//! # Capturing with async move
//!
//! Demonstrates how `move` closures capture their environment by value,
//! enabling them to outlive their creating scope - essential for async tasks.
use std::sync::{Arc, Mutex};
use std::thread;
/// Creates a greeter closure that captures the name by value.
/// The returned closure owns `name` and can be called from anywhere.
pub fn make_greeter(name: String) -> impl Fn() -> String {
move || format!("Hello, {}!", name)
}
/// Creates a counter closure that maintains mutable state.
/// Each call increments and returns the previous value.
pub fn make_counter(start: i32) -> impl FnMut() -> i32 {
let mut count = start;
move || {
let current = count;
count += 1;
current
}
}
/// Creates a stateful accumulator that can be reset.
pub fn make_accumulator() -> impl FnMut(i32) -> i32 {
let mut total = 0;
move |delta| {
total += delta;
total
}
}
/// Demonstrates shared state across threads using Arc<Mutex<T>>.
/// Each thread increments a shared counter - the pattern for async move blocks.
pub fn shared_counter_demo(num_threads: usize) -> i32 {
let shared = Arc::new(Mutex::new(0));
let handles: Vec<_> = (0..num_threads)
.map(|_| {
let shared = Arc::clone(&shared); // Clone Arc before moving
thread::spawn(move || {
// Each thread owns its Arc handle
let mut guard = shared.lock().unwrap();
*guard += 1;
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let result = *shared.lock().unwrap();
result
}
/// Demonstrates capturing multiple values in a move closure.
pub fn compute_with_context(base: i32, multiplier: i32, offset: i32) -> impl FnOnce(i32) -> i32 {
move |x| (base + x) * multiplier + offset
}
/// Factory that creates worker closures with captured configuration.
pub fn make_workers(prefix: String, count: usize) -> Vec<Box<dyn Fn(i32) -> String + Send>> {
(0..count)
.map(|id| {
let prefix = prefix.clone(); // Clone for each closure
Box::new(move |value: i32| format!("{}-worker-{}: {}", prefix, id, value))
as Box<dyn Fn(i32) -> String + Send>
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greeter_captures_name() {
let greet = make_greeter("Alice".to_string());
assert_eq!(greet(), "Hello, Alice!");
}
#[test]
fn test_counter_increments() {
let mut counter = make_counter(10);
assert_eq!(counter(), 10);
assert_eq!(counter(), 11);
assert_eq!(counter(), 12);
}
#[test]
fn test_accumulator() {
let mut acc = make_accumulator();
assert_eq!(acc(5), 5);
assert_eq!(acc(3), 8);
assert_eq!(acc(-2), 6);
}
#[test]
fn test_shared_counter_counts_all_threads() {
let result = shared_counter_demo(5);
assert_eq!(result, 5);
}
#[test]
fn test_compute_with_context() {
let compute = compute_with_context(10, 2, 5);
// (10 + 3) * 2 + 5 = 31
assert_eq!(compute(3), 31);
}
#[test]
fn test_make_workers() {
let workers = make_workers("test".to_string(), 3);
assert_eq!(workers.len(), 3);
assert_eq!(workers[0](42), "test-worker-0: 42");
assert_eq!(workers[1](100), "test-worker-1: 100");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greeter_captures_name() {
let greet = make_greeter("Alice".to_string());
assert_eq!(greet(), "Hello, Alice!");
}
#[test]
fn test_counter_increments() {
let mut counter = make_counter(10);
assert_eq!(counter(), 10);
assert_eq!(counter(), 11);
assert_eq!(counter(), 12);
}
#[test]
fn test_accumulator() {
let mut acc = make_accumulator();
assert_eq!(acc(5), 5);
assert_eq!(acc(3), 8);
assert_eq!(acc(-2), 6);
}
#[test]
fn test_shared_counter_counts_all_threads() {
let result = shared_counter_demo(5);
assert_eq!(result, 5);
}
#[test]
fn test_compute_with_context() {
let compute = compute_with_context(10, 2, 5);
// (10 + 3) * 2 + 5 = 31
assert_eq!(compute(3), 31);
}
#[test]
fn test_make_workers() {
let workers = make_workers("test".to_string(), 3);
assert_eq!(workers.len(), 3);
assert_eq!(workers[0](42), "test-worker-0: 42");
assert_eq!(workers[1](100), "test-worker-1: 100");
}
}
Deep Comparison
OCaml vs Rust: Move Closures
Greeter Factory
OCaml:
let make_greeter name = fun () -> Printf.printf "Hello, %s!\n" name
Rust:
fn make_greeter(name: String) -> impl Fn() {
move || println!("Hello, {name}!")
}
Counter with State
OCaml:
let make_counter start =
let count = ref start in
fun () -> let v = !count in incr count; v
Rust:
fn make_counter(start: i32) -> impl FnMut() -> i32 {
let mut count = start;
move || { let v = count; count += 1; v }
}
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Capture | Implicit by reference | Explicit move keyword |
| Mutable state | ref cell | mut variable in closure |
| Shared ownership | GC handles | Arc::clone() pattern |
| Thread safety | GIL / manual | Enforced by Send/Sync |
Exercises
Arc<Mutex<Vec<String>>> to collect results from multiple threads into a shared accumulator.move.