ExamplesBy LevelBy TopicLearning Paths
505 Intermediate

Boxing Closures

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Boxing Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Every Rust closure has a unique anonymous type — two closures with identical signatures have different types and cannot be stored in the same `Vec` or struct field. Key difference from OCaml: 1. **Boxing requirement**: Rust needs `Box<dyn Fn>` for heterogeneous closure collections; OCaml's uniform representation handles this natively.

Tutorial

The Problem

Every Rust closure has a unique anonymous type — two closures with identical signatures have different types and cannot be stored in the same Vec or struct field. Box<dyn Fn> resolves this by heap-allocating the closure and storing only a fat pointer (address + vtable). This is the mechanism behind: event handler registries, middleware chains, dependency injection containers, and any pattern that selects behaviour at runtime. The cost is one heap allocation per boxed closure and one vtable lookup per call — acceptable for most use cases.

🎯 Learning Outcomes

  • • Define type aliases BoxedFn = Box<dyn Fn(i32) -> i32> for ergonomics
  • • Store multiple closures with different captures in a HashMap<String, BoxedFn>
  • • Chain Vec<BoxedFn> closures with fold for pipeline execution
  • • Select closures at runtime with match/string dispatch
  • • Build a ClosureVec that accepts impl Fn + 'static and stores as Box<dyn Fn>
  • Code Example

    #![allow(clippy::all)]
    //! # Boxing Closures — Dynamic Dispatch
    
    use std::collections::HashMap;
    
    /// Box closure for dynamic dispatch
    pub type BoxedFn = Box<dyn Fn(i32) -> i32>;
    pub type BoxedFnMut = Box<dyn FnMut(i32) -> i32>;
    pub type BoxedFnOnce = Box<dyn FnOnce(i32) -> i32>;
    
    pub fn make_boxed_adder(n: i32) -> BoxedFn {
        Box::new(move |x| x + n)
    }
    
    /// Store different closures in a collection
    pub fn closure_map() -> HashMap<String, BoxedFn> {
        let mut map: HashMap<String, BoxedFn> = HashMap::new();
        map.insert("double".into(), Box::new(|x| x * 2));
        map.insert("square".into(), Box::new(|x| x * x));
        map.insert("negate".into(), Box::new(|x| -x));
        map
    }
    
    /// Chain of boxed closures
    pub fn chain_closures(closures: Vec<BoxedFn>, value: i32) -> i32 {
        closures.iter().fold(value, |acc, f| f(acc))
    }
    
    /// Conditional closure selection
    pub fn select_operation(op: &str) -> Option<BoxedFn> {
        match op {
            "add1" => Some(Box::new(|x| x + 1)),
            "double" => Some(Box::new(|x| x * 2)),
            "square" => Some(Box::new(|x| x * x)),
            _ => None,
        }
    }
    
    /// Vector of closures
    pub struct ClosureVec {
        closures: Vec<BoxedFn>,
    }
    
    impl ClosureVec {
        pub fn new() -> Self {
            Self {
                closures: Vec::new(),
            }
        }
    
        pub fn add<F: Fn(i32) -> i32 + 'static>(&mut self, f: F) {
            self.closures.push(Box::new(f));
        }
    
        pub fn apply_all(&self, x: i32) -> Vec<i32> {
            self.closures.iter().map(|f| f(x)).collect()
        }
    }
    
    impl Default for ClosureVec {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_adder() {
            let add5 = make_boxed_adder(5);
            assert_eq!(add5(10), 15);
        }
    
        #[test]
        fn test_closure_map() {
            let ops = closure_map();
            assert_eq!(ops.get("double").unwrap()(5), 10);
            assert_eq!(ops.get("square").unwrap()(4), 16);
        }
    
        #[test]
        fn test_chain() {
            let closures: Vec<BoxedFn> = vec![
                Box::new(|x| x + 1),
                Box::new(|x| x * 2),
                Box::new(|x| x - 3),
            ];
            // (5+1)*2-3 = 9
            assert_eq!(chain_closures(closures, 5), 9);
        }
    
        #[test]
        fn test_select() {
            let op = select_operation("double").unwrap();
            assert_eq!(op(21), 42);
        }
    
        #[test]
        fn test_closure_vec() {
            let mut cv = ClosureVec::new();
            cv.add(|x| x + 1);
            cv.add(|x| x * 2);
            cv.add(|x| x - 5);
            assert_eq!(cv.apply_all(10), vec![11, 20, 5]);
        }
    }

    Key Differences

  • Boxing requirement: Rust needs Box<dyn Fn> for heterogeneous closure collections; OCaml's uniform representation handles this natively.
  • **'static bound**: Box<dyn Fn + 'static> requires captured values to have 'static lifetime — no borrowed references to local variables. OCaml's GC has no such constraint.
  • Vtable overhead: Rust's Box<dyn Fn> has a 2-pointer fat pointer and vtable dispatch; impl Fn is zero-overhead. OCaml's closures always use indirect dispatch.
  • Type aliases: Rust's type BoxedFn = Box<dyn Fn(i32) -> i32> is a readability convention; OCaml uses type fn_t = int -> int similarly.
  • OCaml Approach

    OCaml functions are first-class with uniform representation — no boxing is needed for heterogeneous collections:

    let ops : (string * (int -> int)) list = [
      ("double", fun x -> x * 2);
      ("square", fun x -> x * x);
    ]
    
    let chain closures value =
      List.fold_left (fun acc f -> f acc) value closures
    

    OCaml's first-class function values already carry their captured environment via GC-managed closures, so no explicit Box or dynamic dispatch is needed.

    Full Source

    #![allow(clippy::all)]
    //! # Boxing Closures — Dynamic Dispatch
    
    use std::collections::HashMap;
    
    /// Box closure for dynamic dispatch
    pub type BoxedFn = Box<dyn Fn(i32) -> i32>;
    pub type BoxedFnMut = Box<dyn FnMut(i32) -> i32>;
    pub type BoxedFnOnce = Box<dyn FnOnce(i32) -> i32>;
    
    pub fn make_boxed_adder(n: i32) -> BoxedFn {
        Box::new(move |x| x + n)
    }
    
    /// Store different closures in a collection
    pub fn closure_map() -> HashMap<String, BoxedFn> {
        let mut map: HashMap<String, BoxedFn> = HashMap::new();
        map.insert("double".into(), Box::new(|x| x * 2));
        map.insert("square".into(), Box::new(|x| x * x));
        map.insert("negate".into(), Box::new(|x| -x));
        map
    }
    
    /// Chain of boxed closures
    pub fn chain_closures(closures: Vec<BoxedFn>, value: i32) -> i32 {
        closures.iter().fold(value, |acc, f| f(acc))
    }
    
    /// Conditional closure selection
    pub fn select_operation(op: &str) -> Option<BoxedFn> {
        match op {
            "add1" => Some(Box::new(|x| x + 1)),
            "double" => Some(Box::new(|x| x * 2)),
            "square" => Some(Box::new(|x| x * x)),
            _ => None,
        }
    }
    
    /// Vector of closures
    pub struct ClosureVec {
        closures: Vec<BoxedFn>,
    }
    
    impl ClosureVec {
        pub fn new() -> Self {
            Self {
                closures: Vec::new(),
            }
        }
    
        pub fn add<F: Fn(i32) -> i32 + 'static>(&mut self, f: F) {
            self.closures.push(Box::new(f));
        }
    
        pub fn apply_all(&self, x: i32) -> Vec<i32> {
            self.closures.iter().map(|f| f(x)).collect()
        }
    }
    
    impl Default for ClosureVec {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_adder() {
            let add5 = make_boxed_adder(5);
            assert_eq!(add5(10), 15);
        }
    
        #[test]
        fn test_closure_map() {
            let ops = closure_map();
            assert_eq!(ops.get("double").unwrap()(5), 10);
            assert_eq!(ops.get("square").unwrap()(4), 16);
        }
    
        #[test]
        fn test_chain() {
            let closures: Vec<BoxedFn> = vec![
                Box::new(|x| x + 1),
                Box::new(|x| x * 2),
                Box::new(|x| x - 3),
            ];
            // (5+1)*2-3 = 9
            assert_eq!(chain_closures(closures, 5), 9);
        }
    
        #[test]
        fn test_select() {
            let op = select_operation("double").unwrap();
            assert_eq!(op(21), 42);
        }
    
        #[test]
        fn test_closure_vec() {
            let mut cv = ClosureVec::new();
            cv.add(|x| x + 1);
            cv.add(|x| x * 2);
            cv.add(|x| x - 5);
            assert_eq!(cv.apply_all(10), vec![11, 20, 5]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_boxed_adder() {
            let add5 = make_boxed_adder(5);
            assert_eq!(add5(10), 15);
        }
    
        #[test]
        fn test_closure_map() {
            let ops = closure_map();
            assert_eq!(ops.get("double").unwrap()(5), 10);
            assert_eq!(ops.get("square").unwrap()(4), 16);
        }
    
        #[test]
        fn test_chain() {
            let closures: Vec<BoxedFn> = vec![
                Box::new(|x| x + 1),
                Box::new(|x| x * 2),
                Box::new(|x| x - 3),
            ];
            // (5+1)*2-3 = 9
            assert_eq!(chain_closures(closures, 5), 9);
        }
    
        #[test]
        fn test_select() {
            let op = select_operation("double").unwrap();
            assert_eq!(op(21), 42);
        }
    
        #[test]
        fn test_closure_vec() {
            let mut cv = ClosureVec::new();
            cv.add(|x| x + 1);
            cv.add(|x| x * 2);
            cv.add(|x| x - 5);
            assert_eq!(cv.apply_all(10), vec![11, 20, 5]);
        }
    }

    Deep Comparison

    Boxing Closures: Comparison

    See src/lib.rs for the Rust implementation.

    Exercises

  • Middleware stack: Build a struct Middleware { handlers: Vec<Box<dyn Fn(Request) -> Response>> } where each handler can short-circuit the chain by returning early.
  • Event emitter: Implement EventEmitter<E> with a HashMap<String, Vec<Box<dyn Fn(&E)>>> that registers and fires named events.
  • **impl Fn vs. Box<dyn Fn> benchmark**: Use criterion to measure the call overhead of Box<dyn Fn(i32)->i32> vs. impl Fn(i32)->i32 for 10 million iterations.
  • Open Source Repos