ExamplesBy LevelBy TopicLearning Paths
543 Intermediate

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