🦀 Functional Rust
🎬 Traits & Generics Shared behaviour, static vs dynamic dispatch, zero-cost polymorphism.
📝 Text version (for readers / accessibility)

• Traits define shared behaviour — like interfaces but with default implementations

• Generics with trait bounds: fn process(item: T) — monomorphized at compile time

• Static dispatch (impl Trait) = zero cost; dynamic dispatch (dyn Trait) = runtime flexibility via vtable

• Blanket implementations apply traits to all types matching a bound

• Associated types and supertraits enable complex type relationships

124: dyn Trait — Dynamic Dispatch

Difficulty: 3 Level: Intermediate Dispatch method calls at runtime via a vtable — the right tool when you need heterogeneous collections or can't know the concrete type at compile time.

The Problem This Solves

`impl Trait` and generics are great for performance, but they have a fundamental limitation: the compiler must know the concrete type at compile time. You can't put a `Circle`, a `Rectangle`, and a `Triangle` in the same `Vec<impl Shape>` — they're different types, they have different sizes, and `impl Trait` produces a different monomorphized copy of the code for each. If you need a collection that holds mixed types, or if you're building a plugin system where the types aren't known at compile time, you need runtime dispatch. `dyn Trait` is Rust's answer. A `Box<dyn Shape>` is a fat pointer: one pointer to the data, one pointer to a vtable. The vtable tells the runtime which concrete `area()` method to call. The cost is an extra pointer indirection per call — usually negligible, always explicit. There's a third option: enum dispatch. If the set of types is closed (you control them all), put them in an enum and `match`. No vtable, no heap allocation, exhaustive matching enforced by the compiler. The shape example in this module shows all three strategies side by side so you can compare the trade-offs directly.

The Intuition

`dyn Trait` = one function call goes through a vtable at runtime — use it when you need heterogeneous collections or open extensibility, and accept the indirection cost.

How It Works in Rust

trait Shape {
 fn area(&self) -> f64;
 fn name(&self) -> &str;
}

// Static dispatch: compiler generates a separate copy for Circle, Rect, etc.
// Fast — direct call, LLVM can inline. But can't mix in one Vec.
fn print_area(s: &impl Shape) {
 println!("{}: {:.2}", s.name(), s.area());
}

// Dynamic dispatch: one function, vtable lookup per call.
// Slightly slower. Can hold Circle, Rect, Triangle in the same Vec.
fn total_area(shapes: &[Box<dyn Shape>]) -> f64 {
 shapes.iter().map(|s| s.area()).sum()  // each .area() goes through vtable
}

let shapes: Vec<Box<dyn Shape>> = vec![
 Box::new(Circle { radius: 5.0 }),
 Box::new(Rect { width: 3.0, height: 4.0 }),
 Box::new(Triangle { base: 6.0, height: 3.0 }),
];
println!("Total: {:.2}", total_area(&shapes));

// Borrowed trait objects — no allocation needed
let c = Circle { radius: 1.0 };
let s: &dyn Shape = &c;   // fat pointer: &data + &vtable
println!("{}", s.name());

// Enum dispatch — closed set, zero overhead, exhaustive matching
enum ShapeEnum { Circle(f64), Rect(f64, f64), Triangle(f64, f64) }
impl ShapeEnum {
 fn area(&self) -> f64 {
     match self {
         ShapeEnum::Circle(r)     => PI * r * r,
         ShapeEnum::Rect(w, h)    => w * h,
         ShapeEnum::Triangle(b,h) => 0.5 * b * h,
     }
 }
}

What This Unlocks

Key Differences

StrategyExtensible?OverheadHeterogeneous?
`impl Trait` (static)YesZero — inlinedNo — one concrete type per call site
`dyn Trait` (dynamic)Yesvtable indirectionYes — mixed types in one collection
Enum dispatchNo — closed setZero — `match`Yes — all variants in one type
ConceptOCamlRust
Object polymorphismObjects (structural)`dyn Trait` (nominal, vtable)
Closed-set polymorphismVariants / GADTsEnums + `match`
Open polymorphismFirst-class modules`dyn Trait` or generics
// Example 124: Dynamic Dispatch — dyn Trait vs impl Trait
//
// impl Trait = static dispatch (monomorphization, zero-cost, no vtable)
// dyn Trait = dynamic dispatch (vtable pointer, runtime flexibility)

use std::f64::consts::PI;

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
struct Rect { 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 Rect {
    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" }
}

// Approach 1: Static dispatch with impl Trait (monomorphized)
fn print_area_static(s: &impl Shape) {
    println!("{}: {:.2}", s.name(), s.area());
}

fn approach1() {
    let c = Circle { radius: 5.0 };
    let r = Rect { width: 3.0, height: 4.0 };
    print_area_static(&c); // compiles to direct call
    print_area_static(&r); // separate monomorphized copy
}

// Approach 2: Dynamic dispatch with dyn Trait (vtable)
fn total_area_dynamic(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn approach2() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle { radius: 5.0 }),
        Box::new(Rect { width: 3.0, height: 4.0 }),
        Box::new(Triangle { base: 6.0, height: 3.0 }),
    ];
    let total = total_area_dynamic(&shapes);
    println!("Dynamic total area: {:.2}", total);
    
    // Can also use &dyn Shape (borrowed trait objects)
    for s in &shapes {
        println!("  {}: {:.2}", s.name(), s.area());
    }
}

// Approach 3: Enum dispatch (closed set, no vtable)
enum ShapeEnum {
    Circle(f64),
    Rect(f64, f64),
    Triangle(f64, f64),
}

impl ShapeEnum {
    fn area(&self) -> f64 {
        match self {
            ShapeEnum::Circle(r) => PI * r * r,
            ShapeEnum::Rect(w, h) => w * h,
            ShapeEnum::Triangle(b, h) => 0.5 * b * h,
        }
    }
}

fn approach3() {
    let shapes = vec![
        ShapeEnum::Circle(5.0),
        ShapeEnum::Rect(3.0, 4.0),
        ShapeEnum::Triangle(6.0, 3.0),
    ];
    let total: f64 = shapes.iter().map(|s| s.area()).sum();
    println!("Enum total area: {:.2}", total);
}

fn main() {
    println!("=== Approach 1: Static (impl Trait) ===");
    approach1();
    println!("\n=== Approach 2: Dynamic (dyn Trait) ===");
    approach2();
    println!("\n=== Approach 3: Enum Dispatch ===");
    approach3();
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_static_dispatch() {
        let c = Circle { radius: 1.0 };
        assert!((c.area() - PI).abs() < 1e-10);
    }

    #[test]
    fn test_dynamic_dispatch() {
        let shapes: Vec<Box<dyn Shape>> = vec![
            Box::new(Rect { width: 2.0, height: 3.0 }),
        ];
        assert!((total_area_dynamic(&shapes) - 6.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_enum_dispatch() {
        let s = ShapeEnum::Rect(2.0, 3.0);
        assert!((s.area() - 6.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_trait_object_ref() {
        let c = Circle { radius: 1.0 };
        let s: &dyn Shape = &c;
        assert_eq!(s.name(), "circle");
    }

    #[test]
    fn test_heterogeneous_collection() {
        let shapes: Vec<Box<dyn Shape>> = vec![
            Box::new(Circle { radius: 1.0 }),
            Box::new(Rect { width: 1.0, height: 1.0 }),
        ];
        assert_eq!(shapes.len(), 2);
        assert!(shapes[0].area() > shapes[1].area()); // π > 1
    }
}
(* Example 124: Dynamic Dispatch — dyn Trait vs impl Trait *)

(* OCaml uses virtual dispatch for objects, first-class modules for
   trait-like polymorphism. *)

(* Approach 1: First-class modules — like dyn Trait *)
module type Shape = sig
  val area : unit -> float
  val name : unit -> string
end

let total_area (shapes : (module Shape) list) =
  List.fold_left (fun acc (module S : Shape) -> acc +. S.area ()) 0.0 shapes

let circle r : (module Shape) = (module struct
  let area () = Float.pi *. r *. r
  let name () = "circle"
end)

let rect w h : (module Shape) = (module struct
  let area () = w *. h
  let name () = "rectangle"
end)

let approach1 () =
  let shapes = [circle 5.0; rect 3.0 4.0; circle 2.0] in
  let total = total_area shapes in
  Printf.printf "Total area: %.2f\n" total;
  assert (total > 90.0)

(* Approach 2: Object-oriented style *)
class virtual shape_obj = object
  method virtual area : float
  method virtual name : string
end

class circle_obj r = object
  inherit shape_obj
  method area = Float.pi *. r *. r
  method name = "circle"
end

class rect_obj w h = object
  inherit shape_obj
  method area = w *. h
  method name = "rectangle"
end

let approach2 () =
  let shapes = [new circle_obj 5.0; new rect_obj 3.0 4.0] in
  let total = List.fold_left (fun acc s -> acc +. s#area) 0.0 shapes in
  Printf.printf "OO total: %.2f\n" total

(* Approach 3: Variant-based dispatch — monomorphic *)
type shape_v = Circle of float | Rect of float * float

let area_v = function
  | Circle r -> Float.pi *. r *. r
  | Rect (w, h) -> w *. h

let approach3 () =
  let shapes = [Circle 5.0; Rect (3.0, 4.0)] in
  let total = List.fold_left (fun acc s -> acc +. area_v s) 0.0 shapes in
  Printf.printf "Variant total: %.2f\n" total

let () =
  approach1 ();
  approach2 ();
  approach3 ();
  Printf.printf "✓ All tests passed\n"

📊 Detailed Comparison

Comparison: Dynamic Dispatch

Heterogeneous Collection

OCaml (first-class modules):

🐪 Show OCaml equivalent
let shapes = [circle 5.0; rect 3.0 4.0]  (* (module Shape) list *)
let total = List.fold_left (fun acc (module S : Shape) -> acc +. S.area ()) 0.0 shapes

Rust (dyn Trait):

let shapes: Vec<Box<dyn Shape>> = vec![
 Box::new(Circle { radius: 5.0 }),
 Box::new(Rect { width: 3.0, height: 4.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();

Static vs Dynamic

Rust — static (monomorphized):

fn print_area(s: &impl Shape) { ... }  // one copy per type

Rust — dynamic (vtable):

fn print_area(s: &dyn Shape) { ... }  // one copy, indirect call

Enum Dispatch (Closed Set)

OCaml:

🐪 Show OCaml equivalent
type shape = Circle of float | Rect of float * float
let area = function Circle r -> pi *. r *. r | Rect (w,h) -> w *. h

Rust:

enum Shape { Circle(f64), Rect(f64, f64) }
impl Shape {
 fn area(&self) -> f64 { match self { ... } }
}