ExamplesBy LevelBy TopicLearning Paths
543 Intermediate GitHub

Lifetimes in dyn Trait

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lifetimes in dyn Trait" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Trait objects (`dyn Trait`) are Rust's mechanism for runtime polymorphism — a single `Box<dyn Renderer>` can hold any type implementing `Renderer`. Key difference from OCaml: 1. **Implicit 'static**: Rust's `Box<dyn Trait>` silently requires `'static` — a common source of confusion for beginners; OCaml has no implicit constraint on stored modules or closures.

Tutorial

The Problem

Trait objects (dyn Trait) are Rust's mechanism for runtime polymorphism — a single Box<dyn Renderer> can hold any type implementing Renderer. But trait objects carry an implicit lifetime bound: Box<dyn Renderer> is shorthand for Box<dyn Renderer + 'static>, meaning the underlying type must contain no non-static references. When you need a trait object that borrows from an external scope, you must write Box<dyn Renderer + 'a> explicitly. This is critical for middleware stacks, plugin systems, and any architecture that stores trait objects.

🎯 Learning Outcomes

  • • Why Box<dyn Trait> is Box<dyn Trait + 'static> by default
  • • How to create Box<dyn Trait + 'a> for trait objects borrowing from a scope
  • • How BorrowingRenderer<'a> implementing Renderer can be stored as Box<dyn Renderer + 'a>
  • • How lifetime-annotated trait objects work in function signatures and structs
  • • Where this pattern appears: plugin systems, middleware, egui/druid widget trees
  • Code Example

    // Default: Box<dyn Trait> = Box<dyn Trait + 'static>
    pub fn store(r: Box<dyn Renderer>) -> Box<dyn Renderer> { r }
    
    // With borrowed data: explicit lifetime
    pub fn use_borrowed<'a>(r: &'a dyn Renderer) -> String {
        r.render()
    }
    
    // Struct with borrowed field needs lifetime on dyn
    struct Container<'a> {
        renderer: Box<dyn Renderer + 'a>,
    }

    Key Differences

  • Implicit 'static: Rust's Box<dyn Trait> silently requires 'static — a common source of confusion for beginners; OCaml has no implicit constraint on stored modules or closures.
  • Lifetime propagation: When a Rust trait object borrows from a scope, that 'a propagates through every type that stores or passes the object; OCaml has no propagation.
  • Plugin systems: Rust plugins stored as Box<dyn Plugin> must be 'static or carefully parameterized; OCaml plugins have no such restriction.
  • Error messages: Missing lifetime on Box<dyn Trait + 'a> gives cryptic "does not live long enough" errors; the fix is always to add + 'a to the trait object type.
  • OCaml Approach

    OCaml achieves runtime polymorphism through first-class modules or abstract types. There are no lifetime constraints on module values — the GC ensures all referenced data remains valid:

    module type Renderer = sig
      val render : unit -> string
    end
    let store_renderer (module R : Renderer) = (module R : Renderer)
    

    Any module satisfying Renderer can be stored regardless of what data it references.

    Full Source

    #![allow(clippy::all)]
    //! Lifetimes in dyn Trait
    //!
    //! Lifetime bounds on trait objects.
    
    use std::fmt;
    
    pub trait Renderer: fmt::Debug {
        fn render(&self) -> String;
    }
    
    /// Box<dyn Renderer> = Box<dyn Renderer + 'static>
    #[derive(Debug)]
    pub struct HtmlRenderer {
        template: String,
    }
    
    impl Renderer for HtmlRenderer {
        fn render(&self) -> String {
            format!("<html>{}</html>", self.template)
        }
    }
    
    /// Store 'static renderer.
    pub fn store_renderer(r: Box<dyn Renderer>) -> Box<dyn Renderer> {
        r
    }
    
    /// Renderer that borrows (needs lifetime).
    #[derive(Debug)]
    pub struct BorrowingRenderer<'a> {
        content: &'a str,
    }
    
    impl<'a> Renderer for BorrowingRenderer<'a> {
        fn render(&self) -> String {
            self.content.to_string()
        }
    }
    
    /// Accept borrowed renderer with explicit lifetime.
    pub fn use_borrowed_renderer<'a>(r: &'a dyn Renderer) -> String {
        r.render()
    }
    
    /// Vec of trait objects (must be 'static).
    pub fn collect_renderers() -> Vec<Box<dyn Renderer>> {
        vec![
            Box::new(HtmlRenderer {
                template: "hello".into(),
            }),
            Box::new(HtmlRenderer {
                template: "world".into(),
            }),
        ]
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_html_renderer() {
            let r = HtmlRenderer {
                template: "content".into(),
            };
            assert_eq!(r.render(), "<html>content</html>");
        }
    
        #[test]
        fn test_store_renderer() {
            let r: Box<dyn Renderer> = Box::new(HtmlRenderer {
                template: "x".into(),
            });
            let r2 = store_renderer(r);
            assert!(r2.render().contains("x"));
        }
    
        #[test]
        fn test_borrowing_renderer() {
            let content = String::from("borrowed");
            let r = BorrowingRenderer { content: &content };
            assert_eq!(r.render(), "borrowed");
        }
    
        #[test]
        fn test_use_borrowed() {
            let r = HtmlRenderer {
                template: "test".into(),
            };
            let result = use_borrowed_renderer(&r);
            assert!(result.contains("test"));
        }
    
        #[test]
        fn test_collect_renderers() {
            let renderers = collect_renderers();
            assert_eq!(renderers.len(), 2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_html_renderer() {
            let r = HtmlRenderer {
                template: "content".into(),
            };
            assert_eq!(r.render(), "<html>content</html>");
        }
    
        #[test]
        fn test_store_renderer() {
            let r: Box<dyn Renderer> = Box::new(HtmlRenderer {
                template: "x".into(),
            });
            let r2 = store_renderer(r);
            assert!(r2.render().contains("x"));
        }
    
        #[test]
        fn test_borrowing_renderer() {
            let content = String::from("borrowed");
            let r = BorrowingRenderer { content: &content };
            assert_eq!(r.render(), "borrowed");
        }
    
        #[test]
        fn test_use_borrowed() {
            let r = HtmlRenderer {
                template: "test".into(),
            };
            let result = use_borrowed_renderer(&r);
            assert!(result.contains("test"));
        }
    
        #[test]
        fn test_collect_renderers() {
            let renderers = collect_renderers();
            assert_eq!(renderers.len(), 2);
        }
    }

    Deep Comparison

    OCaml vs Rust: Trait Object Lifetimes

    OCaml

    (* Objects don't have explicit lifetimes *)
    class type renderer = object
      method render : string
    end
    
    let store (r : renderer) = r
    

    Rust

    // Default: Box<dyn Trait> = Box<dyn Trait + 'static>
    pub fn store(r: Box<dyn Renderer>) -> Box<dyn Renderer> { r }
    
    // With borrowed data: explicit lifetime
    pub fn use_borrowed<'a>(r: &'a dyn Renderer) -> String {
        r.render()
    }
    
    // Struct with borrowed field needs lifetime on dyn
    struct Container<'a> {
        renderer: Box<dyn Renderer + 'a>,
    }
    

    Key Differences

  • OCaml: Objects don't track borrowed references
  • Rust: dyn Trait has implicit or explicit lifetime
  • Rust: 'static default for owned trait objects
  • Rust: Explicit 'a needed for borrowed data
  • Both: Runtime polymorphism via indirection
  • Exercises

  • Scoped renderer: Write a function fn render_with<'a>(content: &'a str, renderer: &dyn Renderer) -> String that uses a borrowed trait object — verify it compiles without a + 'a bound on the renderer since the renderer doesn't capture content.
  • Vec of renderers: Create Vec<Box<dyn Renderer>> (static) and add multiple renderer types — then try creating Vec<Box<dyn Renderer + '_>> containing a BorrowingRenderer and observe the lifetime constraint.
  • Trait object field: Implement struct Screen<'a> { components: Vec<Box<dyn Renderer + 'a>> } with a render_all method that collects all renders into a Vec<String>.
  • Open Source Repos