Lifetimes in Closures
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lifetimes in Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Closures that capture references introduce lifetime constraints that must appear in the closure's type. Key difference from OCaml: 1. **Lifetime bound in return type**: Rust requires `+ 'a` when a returned closure captures a reference; OCaml requires no annotation since the GC keeps captured values alive.
Tutorial
The Problem
Closures that capture references introduce lifetime constraints that must appear in the closure's type. A closure that borrows a &str prefix can only be called while that prefix is alive — the closure's lifetime is bounded by its captured borrows. When returning such closures from functions, the + 'a lifetime bound must appear in the return type to tell callers how long they can use the closure. This is distinct from closures that capture owned data — those have no borrowed lifetime and can be 'static.
🎯 Learning Outcomes
impl Fn(&str) -> String + 'a expresses a closure tied to its captured reference's lifetimelet sum = data.iter().sum()) avoids a lifetime constraintimpl FnMut() -> i32 with mutable state (counter) works with no lifetime annotationBox<dyn Fn(&str) -> String> is needed vs impl Fn for returning closures&'a str (tied) and capturing String (not tied)Code Example
// Closure lifetime bounded by captured reference
pub fn make_prefixer<'a>(prefix: &'a str) -> impl Fn(&str) -> String + 'a {
move |s| format!("{}{}", prefix, s)
}
// + 'a bounds closure lifetime to prefix lifetime
// Closure invalid after prefix droppedKey Differences
+ 'a when a returned closure captures a reference; OCaml requires no annotation since the GC keeps captured values alive.&str (lifetime-bounded) from those capturing String (lifetime-free); OCaml treats all captures uniformly.FnMut() -> i32 with let mut count = 0 captures by move — no lifetime needed; OCaml uses ref cells captured by the GC closure.Box<dyn Fn> for closures returned from structs or stored in heterogeneous collections; OCaml's uniform value representation avoids this distinction.OCaml Approach
OCaml closures capture by reference to the GC heap — no lifetime annotations are needed:
let make_prefixer prefix = fun s -> prefix ^ s
let make_sum_adder data = let sum = List.fold_left (+) 0 data in fun x -> x + sum
let make_counter () = let count = ref 0 in fun () -> incr count; !count
All captured values are GC-managed — there is no concept of a closure's lifetime being bounded by its captures.
Full Source
#![allow(clippy::all)]
//! Lifetimes in Closures
//!
//! Captured references constrain closure lifetimes.
/// Closure capturing reference — bounded by capture lifetime.
pub fn make_prefixer<'a>(prefix: &'a str) -> impl Fn(&str) -> String + 'a {
move |s| format!("{}{}", prefix, s)
}
/// Closure capturing computed value (no lifetime needed).
pub fn make_sum_adder(data: &[i32]) -> impl Fn(i32) -> i32 {
let sum: i32 = data.iter().sum(); // compute before capture
move |x| x + sum
}
/// Closure with explicit lifetime bound.
pub fn make_checker<'a>(valid: &'a [&str]) -> impl Fn(&str) -> bool + 'a {
move |s| valid.contains(&s)
}
/// FnMut closure with state.
pub fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
/// Returning closure that captures local — needs boxing.
pub fn make_formatter(width: usize) -> Box<dyn Fn(&str) -> String> {
Box::new(move |s| format!("{:>width$}", s, width = width))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_prefixer() {
let prefix = String::from("Hello, ");
let greet = make_prefixer(&prefix);
assert_eq!(greet("World"), "Hello, World");
}
#[test]
fn test_make_sum_adder() {
let data = vec![1, 2, 3, 4, 5];
let adder = make_sum_adder(&data);
assert_eq!(adder(10), 25); // 15 + 10
}
#[test]
fn test_make_checker() {
let valid = ["a", "b", "c"];
let check = make_checker(&valid);
assert!(check("a"));
assert!(!check("d"));
}
#[test]
fn test_make_counter() {
let mut counter = make_counter();
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
assert_eq!(counter(), 3);
}
#[test]
fn test_make_formatter() {
let fmt = make_formatter(10);
assert_eq!(fmt("hi"), " hi");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_prefixer() {
let prefix = String::from("Hello, ");
let greet = make_prefixer(&prefix);
assert_eq!(greet("World"), "Hello, World");
}
#[test]
fn test_make_sum_adder() {
let data = vec![1, 2, 3, 4, 5];
let adder = make_sum_adder(&data);
assert_eq!(adder(10), 25); // 15 + 10
}
#[test]
fn test_make_checker() {
let valid = ["a", "b", "c"];
let check = make_checker(&valid);
assert!(check("a"));
assert!(!check("d"));
}
#[test]
fn test_make_counter() {
let mut counter = make_counter();
assert_eq!(counter(), 1);
assert_eq!(counter(), 2);
assert_eq!(counter(), 3);
}
#[test]
fn test_make_formatter() {
let fmt = make_formatter(10);
assert_eq!(fmt("hi"), " hi");
}
}
Deep Comparison
OCaml vs Rust: Closure Lifetimes
OCaml
(* Closures capture freely — GC manages *)
let make_prefixer prefix =
fun s -> prefix ^ s
let prefixer = make_prefixer "Hello, "
(* prefix kept alive by closure *)
Rust
// Closure lifetime bounded by captured reference
pub fn make_prefixer<'a>(prefix: &'a str) -> impl Fn(&str) -> String + 'a {
move |s| format!("{}{}", prefix, s)
}
// + 'a bounds closure lifetime to prefix lifetime
// Closure invalid after prefix dropped
Key Differences
Exercises
fn make_both<'a>(f: impl Fn(&str) -> bool + 'a, g: impl Fn(&str) -> bool + 'a) -> impl Fn(&str) -> bool + 'a that returns a closure checking both.make_checker to clone the valid slice data into an owned Vec<String> inside the closure so no lifetime annotation is needed on the return type.make_counter to return a pair (impl FnMut() -> i32, impl FnMut()) where the second closure resets the counter — verify both closures share the same mutable state.