Complex Closure Environments
Tutorial Video
Text description (accessibility)
This video demonstrates the "Complex Closure Environments" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Closures derive much of their power from capturing their surrounding environment — local variables, structs, collections, even other closures. Key difference from OCaml: 1. **Capture semantics**: Rust closures capture by reference by default but require `move` to take ownership; OCaml always captures by reference to GC
Tutorial
The Problem
Closures derive much of their power from capturing their surrounding environment — local variables, structs, collections, even other closures. Understanding what a closure captures, how it captures it (by move vs by reference), and what that means for ownership is central to writing idiomatic Rust. This example explores complex capture scenarios: closures over structs with boxed function fields, cyclic iterators over vectors, closures wrapping other closures, mutable counters, and growing accumulators.
🎯 Learning Outcomes
move closures take ownership of captured variablesBox<dyn Fn> fieldFnMut closuresFn/FnMut/FnOnce trait boundCode Example
pub fn make_counter(start: i32) -> impl FnMut() -> i32 {
let mut count = start;
move || {
let current = count;
count += 1;
current
}
}
pub fn make_cycler<T: Clone>(items: Vec<T>) -> impl FnMut() -> T {
let mut index = 0;
move || {
let val = items[index].clone();
index = (index + 1) % items.len();
val
}
}Key Differences
move to take ownership; OCaml always captures by reference to GC-managed heap values.FnMut for closures that mutate captured state, making the mutation explicit at the type level; OCaml uses ref cells with no type-level distinction.'static if stored); OCaml has no such constraint.Vec and index completely inside the closure; OCaml's equivalent uses ref and an array, with the GC preventing dangling references.OCaml Approach
OCaml closures capture by reference to the heap — all values are boxed, so there is no move/copy distinction. A mutable counter is represented with ref:
let make_cycler items =
let arr = Array.of_list items in
let i = ref 0 in
fun () ->
let v = arr.(!i) in
i := (!i + 1) mod Array.length arr;
v
Wrapping a closure with logging is identical syntactically — just fun x -> log name; f x.
Full Source
#![allow(clippy::all)]
//! Complex Closure Environments
//!
//! Closures capturing structs, collections, and other closures.
/// Configuration for a formatter.
pub struct Config {
pub prefix: String,
pub max_len: usize,
pub transform: Box<dyn Fn(String) -> String>,
}
/// Closure capturing a Config struct.
pub fn make_formatter(cfg: Config) -> impl FnMut(&str) -> String {
move |s: &str| {
let truncated = if s.len() > cfg.max_len {
format!("{}...", &s[..cfg.max_len])
} else {
s.to_string()
};
(cfg.transform)(format!("{}{}", cfg.prefix, truncated))
}
}
/// Closure capturing a Vec and an index — cyclic iterator.
pub fn make_cycler<T: Clone>(items: Vec<T>) -> impl FnMut() -> T {
let mut index = 0;
move || {
let val = items[index].clone();
index = (index + 1) % items.len();
val
}
}
/// Closure capturing another closure.
pub fn make_logged_fn<A, B, F>(f: F, name: &str) -> impl Fn(A) -> B
where
F: Fn(A) -> B,
{
let name = name.to_string();
move |a| {
// In real code, this would log
let _ = &name; // use the captured name
f(a)
}
}
/// Counter that captures mutable state.
pub fn make_counter(start: i32) -> impl FnMut() -> i32 {
let mut count = start;
move || {
let current = count;
count += 1;
current
}
}
/// Accumulator that captures a Vec.
pub fn make_accumulator<T: Clone>() -> impl FnMut(T) -> Vec<T> {
let mut items: Vec<T> = Vec::new();
move |item: T| {
items.push(item);
items.clone()
}
}
/// Closure capturing a HashMap.
pub fn make_cache<K, V, F>(compute: F) -> impl FnMut(K) -> V
where
K: std::hash::Hash + Eq + Clone,
V: Clone,
F: Fn(&K) -> V,
{
let mut cache = std::collections::HashMap::new();
move |key: K| {
cache
.entry(key.clone())
.or_insert_with(|| compute(&key))
.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_formatter() {
let cfg = Config {
prefix: "[INFO] ".to_string(),
max_len: 10,
transform: Box::new(|s| s.to_uppercase()),
};
let mut fmt = make_formatter(cfg);
assert_eq!(fmt("hello"), "[INFO] HELLO");
assert_eq!(fmt("this is a very long message"), "[INFO] THIS IS A ...");
}
#[test]
fn test_make_cycler() {
let mut cycler = make_cycler(vec!["a", "b", "c"]);
assert_eq!(cycler(), "a");
assert_eq!(cycler(), "b");
assert_eq!(cycler(), "c");
assert_eq!(cycler(), "a"); // wraps around
}
#[test]
fn test_make_counter() {
let mut counter = make_counter(10);
assert_eq!(counter(), 10);
assert_eq!(counter(), 11);
assert_eq!(counter(), 12);
}
#[test]
fn test_make_accumulator() {
let mut acc = make_accumulator();
assert_eq!(acc(1), vec![1]);
assert_eq!(acc(2), vec![1, 2]);
assert_eq!(acc(3), vec![1, 2, 3]);
}
#[test]
fn test_make_cache() {
use std::cell::Cell;
let call_count = Cell::new(0);
let mut cached_square = make_cache(|&x: &i32| {
call_count.set(call_count.get() + 1);
x * x
});
assert_eq!(cached_square(5), 25);
assert_eq!(call_count.get(), 1);
assert_eq!(cached_square(5), 25); // cached
assert_eq!(call_count.get(), 1);
assert_eq!(cached_square(3), 9);
assert_eq!(call_count.get(), 2);
}
#[test]
fn test_make_logged_fn() {
let double = make_logged_fn(|x: i32| x * 2, "double");
assert_eq!(double(21), 42);
}
#[test]
fn test_complex_environment() {
let multiplier = 10;
let offset = 5;
let items = vec![1, 2, 3];
// Closure capturing multiple values
let complex = move |i: usize| items.get(i).map(|x| x * multiplier + offset);
assert_eq!(complex(0), Some(15));
assert_eq!(complex(1), Some(25));
assert_eq!(complex(2), Some(35));
assert_eq!(complex(3), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_formatter() {
let cfg = Config {
prefix: "[INFO] ".to_string(),
max_len: 10,
transform: Box::new(|s| s.to_uppercase()),
};
let mut fmt = make_formatter(cfg);
assert_eq!(fmt("hello"), "[INFO] HELLO");
assert_eq!(fmt("this is a very long message"), "[INFO] THIS IS A ...");
}
#[test]
fn test_make_cycler() {
let mut cycler = make_cycler(vec!["a", "b", "c"]);
assert_eq!(cycler(), "a");
assert_eq!(cycler(), "b");
assert_eq!(cycler(), "c");
assert_eq!(cycler(), "a"); // wraps around
}
#[test]
fn test_make_counter() {
let mut counter = make_counter(10);
assert_eq!(counter(), 10);
assert_eq!(counter(), 11);
assert_eq!(counter(), 12);
}
#[test]
fn test_make_accumulator() {
let mut acc = make_accumulator();
assert_eq!(acc(1), vec![1]);
assert_eq!(acc(2), vec![1, 2]);
assert_eq!(acc(3), vec![1, 2, 3]);
}
#[test]
fn test_make_cache() {
use std::cell::Cell;
let call_count = Cell::new(0);
let mut cached_square = make_cache(|&x: &i32| {
call_count.set(call_count.get() + 1);
x * x
});
assert_eq!(cached_square(5), 25);
assert_eq!(call_count.get(), 1);
assert_eq!(cached_square(5), 25); // cached
assert_eq!(call_count.get(), 1);
assert_eq!(cached_square(3), 9);
assert_eq!(call_count.get(), 2);
}
#[test]
fn test_make_logged_fn() {
let double = make_logged_fn(|x: i32| x * 2, "double");
assert_eq!(double(21), 42);
}
#[test]
fn test_complex_environment() {
let multiplier = 10;
let offset = 5;
let items = vec![1, 2, 3];
// Closure capturing multiple values
let complex = move |i: usize| items.get(i).map(|x| x * multiplier + offset);
assert_eq!(complex(0), Some(15));
assert_eq!(complex(1), Some(25));
assert_eq!(complex(2), Some(35));
assert_eq!(complex(3), None);
}
}
Deep Comparison
OCaml vs Rust: Complex Closure Environments
OCaml
let make_counter start =
let count = ref start in
fun () ->
let c = !count in
count := c + 1;
c
let make_cycler items =
let idx = ref 0 in
let arr = Array.of_list items in
fun () ->
let v = arr.(!idx) in
idx := (!idx + 1) mod Array.length arr;
v
Rust
pub fn make_counter(start: i32) -> impl FnMut() -> i32 {
let mut count = start;
move || {
let current = count;
count += 1;
current
}
}
pub fn make_cycler<T: Clone>(items: Vec<T>) -> impl FnMut() -> T {
let mut index = 0;
move || {
let val = items[index].clone();
index = (index + 1) % items.len();
val
}
}
Key Differences
ref for mutable captured valuesmove captures ownership, mut allows mutationExercises
make_throttle(f, n) that calls f only every n invocations, tracking the call count inside the returned FnMut.make_logged_fn to record every call's argument and return value in a Vec captured inside the wrapper closure.make_pipeline(steps: Vec<Box<dyn Fn(String) -> String>>) that returns an impl FnMut(String) -> String applying each step in sequence.