386: Object-Safe Traits
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
Self returns, non-dispatchable methods)Drawable with concrete return types and &self methods satisfies object safetywhere Self: Sized trick to include non-object-safe methods in object-safe traitsCode 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
where Self: Sized allows adding non-object-safe methods to object-safe traits; OCaml has no need for this pattern.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"));
}
}#[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
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.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.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.