Boxing Closures
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
BoxedFn = Box<dyn Fn(i32) -> i32> for ergonomicsHashMap<String, BoxedFn>Vec<BoxedFn> closures with fold for pipeline executionClosureVec 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
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.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 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]);
}
}#[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
struct Middleware { handlers: Vec<Box<dyn Fn(Request) -> Response>> } where each handler can short-circuit the chain by returning early.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.