Lifetimes in dyn Trait
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
Box<dyn Trait> is Box<dyn Trait + 'static> by defaultBox<dyn Trait + 'a> for trait objects borrowing from a scopeBorrowingRenderer<'a> implementing Renderer can be stored as Box<dyn Renderer + 'a>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
Box<dyn Trait> silently requires 'static — a common source of confusion for beginners; OCaml has no implicit constraint on stored modules or closures.'a propagates through every type that stores or passes the object; OCaml has no propagation.Box<dyn Plugin> must be 'static or carefully parameterized; OCaml plugins have no such restriction.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);
}
}#[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
Exercises
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<Box<dyn Renderer>> (static) and add multiple renderer types — then try creating Vec<Box<dyn Renderer + '_>> containing a BorrowingRenderer and observe the lifetime constraint.struct Screen<'a> { components: Vec<Box<dyn Renderer + 'a>> } with a render_all method that collects all renders into a Vec<String>.