ExamplesBy LevelBy TopicLearning Paths
516 Intermediate

Complex Closure Environments

Functional Programming

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

  • • How move closures take ownership of captured variables
  • • How a closure can capture a struct containing a Box<dyn Fn> field
  • • How mutable state (counters, accumulators) lives inside FnMut closures
  • • How closures can wrap other closures to add behavior like logging
  • • The relationship between closure capture mode and the Fn/FnMut/FnOnce trait bound
  • Code 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

  • Capture semantics: Rust closures capture by reference by default but require move to take ownership; OCaml always captures by reference to GC-managed heap values.
  • Mutability in captures: Rust requires FnMut for closures that mutate captured state, making the mutation explicit at the type level; OCaml uses ref cells with no type-level distinction.
  • Nested closures: Rust must satisfy lifetime/ownership rules when a closure captures another closure (e.g., both must be 'static if stored); OCaml has no such constraint.
  • Cycler state: Rust's cycler owns its 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: Uses ref for mutable captured values
  • Rust: move captures ownership, mut allows mutation
  • Both: Closures can capture structs, collections, other closures
  • Rust: FnMut trait indicates the closure mutates its environment
  • Both enable stateful closures with complex captured environments
  • Exercises

  • Throttle closure: Write make_throttle(f, n) that calls f only every n invocations, tracking the call count inside the returned FnMut.
  • Logging wrapper: Extend make_logged_fn to record every call's argument and return value in a Vec captured inside the wrapper closure.
  • Composable formatter: Implement make_pipeline(steps: Vec<Box<dyn Fn(String) -> String>>) that returns an impl FnMut(String) -> String applying each step in sequence.
  • Open Source Repos