ExamplesBy LevelBy TopicLearning Paths
386 Intermediate

386: Object-Safe Traits

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "386: Object-Safe Traits" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Not every Rust trait can be used as `dyn Trait`. Key difference from OCaml: 1. **Safety rules**: Rust has explicit object safety rules enforced at compile time; OCaml has no equivalent restriction on virtual methods.

Tutorial

The Problem

Not every Rust trait can be used as dyn Trait. Object safety rules exist because vtables have fixed layouts: every slot is a function pointer taking an erased *mut () as self. Traits with generic methods (fn map<B>(self, f: impl Fn(A) -> B)) cannot appear in vtables because each monomorphization would need a separate slot. Similarly, methods returning Self or taking Self parameters break the type erasure required for vtables. Understanding these rules prevents compiler errors and guides API design.

Object safety is a prerequisite for plugin architectures, Box<dyn Error>, Box<dyn Iterator>, and any heterogeneous dispatch system in Rust.

🎯 Learning Outcomes

  • • Understand which trait features make a trait non-object-safe (generic methods, Self returns, non-dispatchable methods)
  • • Learn how to design traits that remain object-safe while providing useful functionality
  • • See how Drawable with concrete return types and &self methods satisfies object safety
  • • Understand the where Self: Sized trick to include non-object-safe methods in object-safe traits
  • • Learn how the compiler error messages guide you when violating object safety
  • Code Example

    #![allow(clippy::all)]
    //! Object Safety Rules
    
    pub trait Drawable {
        fn draw(&self) -> String;
        fn area(&self) -> f64;
    }
    
    pub struct Circle {
        pub radius: f64,
    }
    pub struct Rectangle {
        pub width: f64,
        pub height: f64,
    }
    
    impl Drawable for Circle {
        fn draw(&self) -> String {
            format!("Circle(r={})", self.radius)
        }
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    
    impl Drawable for Rectangle {
        fn draw(&self) -> String {
            format!("Rectangle({}x{})", self.width, self.height)
        }
        fn area(&self) -> f64 {
            self.width * self.height
        }
    }
    
    pub fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle_area() {
            let c = Circle { radius: 1.0 };
            assert!((c.area() - std::f64::consts::PI).abs() < 0.001);
        }
        #[test]
        fn test_rect_area() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
        #[test]
        fn test_total_area() {
            let shapes: Vec<Box<dyn Drawable>> = vec![
                Box::new(Circle { radius: 1.0 }),
                Box::new(Rectangle {
                    width: 2.0,
                    height: 3.0,
                }),
            ];
            let total = total_area(&shapes);
            assert!(total > 9.0); // PI + 6
        }
        #[test]
        fn test_draw() {
            let c = Circle { radius: 5.0 };
            assert!(c.draw().contains("Circle"));
        }
    }

    Key Differences

  • Safety rules: Rust has explicit object safety rules enforced at compile time; OCaml has no equivalent restriction on virtual methods.
  • Performance model: Rust makes monomorphized (static dispatch) and vtable (dynamic dispatch) costs explicit and separately controllable; OCaml always uses dynamic dispatch for objects.
  • Workaround: Rust's where Self: Sized allows adding non-object-safe methods to object-safe traits; OCaml has no need for this pattern.
  • Error messages: Rust produces detailed object safety violation errors explaining which method caused the problem; OCaml type errors are at the type unification level.
  • OCaml Approach

    OCaml's object methods are always virtually dispatched — there are no object safety restrictions. A class with a polymorphic method method map : 'a -> 'b is valid. OCaml's approach trades the performance guarantees of monomorphization for flexibility. The equivalent of non-object-safe methods in OCaml is simply methods with polymorphic types, which are allowed.

    Full Source

    #![allow(clippy::all)]
    //! Object Safety Rules
    
    pub trait Drawable {
        fn draw(&self) -> String;
        fn area(&self) -> f64;
    }
    
    pub struct Circle {
        pub radius: f64,
    }
    pub struct Rectangle {
        pub width: f64,
        pub height: f64,
    }
    
    impl Drawable for Circle {
        fn draw(&self) -> String {
            format!("Circle(r={})", self.radius)
        }
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    
    impl Drawable for Rectangle {
        fn draw(&self) -> String {
            format!("Rectangle({}x{})", self.width, self.height)
        }
        fn area(&self) -> f64 {
            self.width * self.height
        }
    }
    
    pub fn total_area(shapes: &[Box<dyn Drawable>]) -> f64 {
        shapes.iter().map(|s| s.area()).sum()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle_area() {
            let c = Circle { radius: 1.0 };
            assert!((c.area() - std::f64::consts::PI).abs() < 0.001);
        }
        #[test]
        fn test_rect_area() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
        #[test]
        fn test_total_area() {
            let shapes: Vec<Box<dyn Drawable>> = vec![
                Box::new(Circle { radius: 1.0 }),
                Box::new(Rectangle {
                    width: 2.0,
                    height: 3.0,
                }),
            ];
            let total = total_area(&shapes);
            assert!(total > 9.0); // PI + 6
        }
        #[test]
        fn test_draw() {
            let c = Circle { radius: 5.0 };
            assert!(c.draw().contains("Circle"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_circle_area() {
            let c = Circle { radius: 1.0 };
            assert!((c.area() - std::f64::consts::PI).abs() < 0.001);
        }
        #[test]
        fn test_rect_area() {
            let r = Rectangle {
                width: 3.0,
                height: 4.0,
            };
            assert_eq!(r.area(), 12.0);
        }
        #[test]
        fn test_total_area() {
            let shapes: Vec<Box<dyn Drawable>> = vec![
                Box::new(Circle { radius: 1.0 }),
                Box::new(Rectangle {
                    width: 2.0,
                    height: 3.0,
                }),
            ];
            let total = total_area(&shapes);
            assert!(total > 9.0); // PI + 6
        }
        #[test]
        fn test_draw() {
            let c = Circle { radius: 5.0 };
            assert!(c.draw().contains("Circle"));
        }
    }

    Deep Comparison

    OCaml vs Rust: 386-object-safe-traits

    Exercises

  • Non-object-safe trait: Write a trait with a generic method (fn transform<B>(&self) -> B) and attempt to use it as dyn Trait. Document the compiler error, then fix it using where Self: Sized to exclude the method from the vtable.
  • Shape renderer: Extend Drawable with a bounding_box(&self) -> (f64, f64, f64, f64) method. Build a renderer that takes Vec<Box<dyn Drawable>>, computes total area, and finds the largest bounding box using only dyn dispatch.
  • Object-safe wrapper: Take a non-object-safe trait Clonable (with fn clone_box(&self) -> Box<dyn Clonable>) and implement it for several types. This is the standard pattern for making Clone work with dyn Trait.
  • Open Source Repos