ExamplesBy LevelBy TopicLearning Paths
870 Intermediate

870-trait-objects — Trait Objects (Dynamic Dispatch)

Functional Programming

Tutorial

The Problem

In object-oriented languages, polymorphism is the default: a method call on a base-class pointer dispatches to the concrete implementation at runtime. Functional languages like OCaml achieve the same through algebraic types with pattern matching (static) or object types (dynamic). Rust offers both mechanisms: generics with trait bounds (static/monomorphized dispatch, zero cost) and trait objects dyn Trait (dynamic dispatch via vtable, runtime polymorphism). The choice between them affects binary size, flexibility, and whether the concrete type must be known at compile time. This example shows both approaches using a Shape hierarchy.

🎯 Learning Outcomes

  • • Understand the difference between static dispatch (impl Trait) and dynamic dispatch (dyn Trait)
  • • Implement a vtable-based polymorphic collection using Box<dyn Trait> or &dyn Trait
  • • Recognize when dynamic dispatch is necessary (heterogeneous collections, plugin systems)
  • • Compare Rust's trait objects with OCaml's object types and polymorphic variants
  • • Understand object safety requirements for traits used as trait objects
  • Code Example

    trait Shape {
        fn area(&self) -> f64;
        fn name(&self) -> &str;
    }
    
    struct Circle { radius: f64 }
    
    impl Shape for Circle {
        fn area(&self) -> f64 { PI * self.radius * self.radius }
        fn name(&self) -> &str { "Circle" }
    }
    
    fn total_area(shapes: &[&dyn Shape]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }

    Key Differences

  • Fat pointers: Rust &dyn Trait is two words (data + vtable); OCaml object values carry a vtable inline in the heap block header.
  • Object safety: Rust trait objects require object-safe traits (no generics in methods, no Self return); OCaml has no such restriction.
  • Heterogeneous collections: Both languages support Vec<Box<dyn Shape>>-style collections; OCaml does it with shape list using object types.
  • Open vs closed: Rust trait objects enable open extension (anyone can implement the trait); OCaml ADTs are closed unless extended with polymorphic variants.
  • OCaml Approach

    OCaml provides two mechanisms. Object types (class type shape = object method area: float ... end) give structural subtyping and dynamic dispatch. Algebraic types with pattern matching (type shape_adt = Circle of float | Rectangle of float * float | Triangle of float * float) give static exhaustive dispatch. The OCaml algebraic approach is more idiomatic for closed hierarchies; object types are used when the hierarchy must be extensible by third parties. Both compile to efficient code; object dispatch uses a vtable like Rust's dyn Trait.

    Full Source

    #![allow(clippy::all)]
    // Example 076: Trait Objects — Dynamic Dispatch
    // OCaml polymorphism → Rust dyn Trait vs generics
    
    use std::f64::consts::PI;
    
    // === Approach 1: Trait objects (dyn Trait) — dynamic dispatch ===
    trait Shape {
        fn area(&self) -> f64;
        fn name(&self) -> &str;
    }
    
    struct Circle {
        radius: f64,
    }
    
    struct Rectangle {
        width: f64,
        height: f64,
    }
    
    struct Triangle {
        base: f64,
        height: f64,
    }
    
    impl Shape for Circle {
        fn area(&self) -> f64 {
            PI * self.radius * self.radius
        }
        fn name(&self) -> &str {
            "Circle"
        }
    }
    
    impl Shape for Rectangle {
        fn area(&self) -> f64 {
            self.width * self.height
        }
        fn name(&self) -> &str {
            "Rectangle"
        }
    }
    
    impl Shape for Triangle {
        fn area(&self) -> f64 {
            0.5 * self.base * self.height
        }
        fn name(&self) -> &str {
            "Triangle"
        }
    }
    
    // Dynamic dispatch: accepts any Shape via trait object
    fn total_area_dyn(shapes: &[&dyn Shape]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    fn describe_dyn(shape: &dyn Shape) -> String {
        format!("{}: area={:.2}", shape.name(), shape.area())
    }
    
    // === Approach 2: Generics (static dispatch / monomorphization) ===
    fn describe_generic<S: Shape>(shape: &S) -> String {
        format!("{}: area={:.2}", shape.name(), shape.area())
    }
    
    fn total_area_generic(shapes: &[Box<dyn Shape>]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    // === Approach 3: Enum dispatch (like OCaml ADT) ===
    enum ShapeEnum {
        Circle(f64),
        Rectangle(f64, f64),
        Triangle(f64, f64),
    }
    
    impl ShapeEnum {
        fn area(&self) -> f64 {
            match self {
                ShapeEnum::Circle(r) => PI * r * r,
                ShapeEnum::Rectangle(w, h) => w * h,
                ShapeEnum::Triangle(b, h) => 0.5 * b * h,
            }
        }
    
        fn name(&self) -> &str {
            match self {
                ShapeEnum::Circle(_) => "Circle",
                ShapeEnum::Rectangle(_, _) => "Rectangle",
                ShapeEnum::Triangle(_, _) => "Triangle",
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle_area() {
            let c = Circle { radius: 5.0 };
            assert!((c.area() - PI * 25.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_rectangle_area() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert!((r.area() - 12.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_triangle_area() {
            let t = Triangle {
                base: 6.0,
                height: 3.0,
            };
            assert!((t.area() - 9.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_dynamic_dispatch_total() {
            let c = Circle { radius: 5.0 };
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            let t = Triangle {
                base: 6.0,
                height: 3.0,
            };
            let shapes: Vec<&dyn Shape> = vec![&c, &r, &t];
            let total = total_area_dyn(&shapes);
            let expected = PI * 25.0 + 12.0 + 9.0;
            assert!((total - expected).abs() < 1e-10);
        }
    
        #[test]
        fn test_enum_dispatch() {
            let c = ShapeEnum::Circle(5.0);
            assert!((c.area() - PI * 25.0).abs() < 1e-10);
            assert_eq!(c.name(), "Circle");
        }
    
        #[test]
        fn test_describe() {
            let c = Circle { radius: 1.0 };
            let desc = describe_generic(&c);
            assert!(desc.starts_with("Circle"));
        }
    
        #[test]
        fn test_boxed_shapes() {
            let shapes: Vec<Box<dyn Shape>> = vec![
                Box::new(Circle { radius: 1.0 }),
                Box::new(Rectangle {
                    width: 2.0,
                    height: 3.0,
                }),
            ];
            let total = total_area_generic(&shapes);
            assert!((total - (PI + 6.0)).abs() < 1e-10);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle_area() {
            let c = Circle { radius: 5.0 };
            assert!((c.area() - PI * 25.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_rectangle_area() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert!((r.area() - 12.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_triangle_area() {
            let t = Triangle {
                base: 6.0,
                height: 3.0,
            };
            assert!((t.area() - 9.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_dynamic_dispatch_total() {
            let c = Circle { radius: 5.0 };
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            let t = Triangle {
                base: 6.0,
                height: 3.0,
            };
            let shapes: Vec<&dyn Shape> = vec![&c, &r, &t];
            let total = total_area_dyn(&shapes);
            let expected = PI * 25.0 + 12.0 + 9.0;
            assert!((total - expected).abs() < 1e-10);
        }
    
        #[test]
        fn test_enum_dispatch() {
            let c = ShapeEnum::Circle(5.0);
            assert!((c.area() - PI * 25.0).abs() < 1e-10);
            assert_eq!(c.name(), "Circle");
        }
    
        #[test]
        fn test_describe() {
            let c = Circle { radius: 1.0 };
            let desc = describe_generic(&c);
            assert!(desc.starts_with("Circle"));
        }
    
        #[test]
        fn test_boxed_shapes() {
            let shapes: Vec<Box<dyn Shape>> = vec![
                Box::new(Circle { radius: 1.0 }),
                Box::new(Rectangle {
                    width: 2.0,
                    height: 3.0,
                }),
            ];
            let total = total_area_generic(&shapes);
            assert!((total - (PI + 6.0)).abs() < 1e-10);
        }
    }

    Deep Comparison

    Comparison: Trait Objects

    Dynamic Dispatch

    OCaml — Object types:

    class type shape = object
      method area : float
      method name : string
    end
    
    class circle r = object
      method area = Float.pi *. r *. r
      method name = "Circle"
    end
    
    let total_area (shapes : shape list) =
      List.fold_left (fun acc s -> acc +. s#area) 0.0 shapes
    

    Rust — dyn Trait:

    trait Shape {
        fn area(&self) -> f64;
        fn name(&self) -> &str;
    }
    
    struct Circle { radius: f64 }
    
    impl Shape for Circle {
        fn area(&self) -> f64 { PI * self.radius * self.radius }
        fn name(&self) -> &str { "Circle" }
    }
    
    fn total_area(shapes: &[&dyn Shape]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    

    Enum-Based (ADT) Dispatch

    OCaml:

    type shape = Circle of float | Rectangle of float * float
    
    let area = function
      | Circle r -> Float.pi *. r *. r
      | Rectangle (w, h) -> w *. h
    

    Rust:

    enum Shape { Circle(f64), Rectangle(f64, f64) }
    
    impl Shape {
        fn area(&self) -> f64 {
            match self {
                Shape::Circle(r) => PI * r * r,
                Shape::Rectangle(w, h) => w * h,
            }
        }
    }
    

    Static Dispatch (Generics)

    OCaml — Functor/constraint:

    module type SHAPE = sig
      type t
      val area : t -> float
    end
    

    Rust — Generic bounds:

    fn describe<S: Shape>(shape: &S) -> String {
        format!("{}: area={:.2}", shape.name(), shape.area())
    }
    

    Exercises

  • Add a Perimeter trait and implement it for all shapes, then compute total perimeter of a Vec<Box<dyn Shape>>.
  • Implement a describe_all function that takes &[Box<dyn Shape>] and returns a formatted summary using Display.
  • Refactor total_area_dyn to use impl Iterator<Item = &dyn Shape> instead of a slice, and explain the difference.
  • Open Source Repos