ExamplesBy LevelBy TopicLearning Paths
544 Intermediate

Lifetimes in Closures

Functional Programming

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

  • • How impl Fn(&str) -> String + 'a expresses a closure tied to its captured reference's lifetime
  • • How computing and capturing an owned value (let sum = data.iter().sum()) avoids a lifetime constraint
  • • How impl FnMut() -> i32 with mutable state (counter) works with no lifetime annotation
  • • When Box<dyn Fn(&str) -> String> is needed vs impl Fn for returning closures
  • • The difference between a closure capturing &'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 dropped

    Key Differences

  • 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.
  • Owned vs borrowed captures: Rust distinguishes closures capturing &str (lifetime-bounded) from those capturing String (lifetime-free); OCaml treats all captures uniformly.
  • Mutable counter: Rust FnMut() -> i32 with let mut count = 0 captures by move — no lifetime needed; OCaml uses ref cells captured by the GC closure.
  • Box vs impl: Rust sometimes requires 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");
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: GC keeps captured values alive
  • Rust: + 'a on impl bounds closure lifetime
  • Rust: Closure can't outlive its captures
  • Rust: Copy types avoid lifetime issues
  • Both: Closures capture environment
  • Exercises

  • Lifetimed combinator: Write 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.
  • Owned capture optimization: Rewrite 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.
  • Counter with reset: Extend 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.
  • Open Source Repos