๐Ÿฆ€ Functional Rust

147: Type Erasure

Difficulty: 4 Level: Advanced Hide a concrete type behind a trait object โ€” keep the behavior, forget the type.

The Problem This Solves

You want to store a `Circle`, a `Rectangle`, and a `Triangle` in the same `Vec`. But they're different types โ€” Rust doesn't allow that. You need a way to say "I only care that each thing can compute its area" and forget what it actually is. Type erasure does exactly that: you erase the concrete type and retain only a trait interface. A `Box<dyn Drawable>` can hold any type that implements `Drawable`. You can call `.area()` on it. You can't call `Circle`-specific methods โ€” those are gone. The trade is flexibility for specificity. The alternative โ€” a `Vec<ShapeEnum>` โ€” is a closed set. Type erasure is open: any type that implements the trait works, even ones defined after the fact.

The Intuition

`Box<dyn Trait>` is a fat pointer: two words of memory. One word points to the data on the heap. The other points to a vtable โ€” a table of function pointers for the trait's methods. When you call `.area()`, Rust looks up the right function in the vtable at runtime. This is dynamic dispatch: the exact function called is determined at runtime, not compile time. You trade monomorphization (zero-cost, compile-time) for flexibility (runtime lookup, one allocation per value). When to use: heterogeneous collections, plugin systems, callbacks, anything where the concrete type is not known until runtime.

How It Works in Rust

pub trait Drawable: std::fmt::Debug {
 fn area(&self) -> f64;
 fn perimeter(&self) -> f64;
 fn name(&self) -> &'static str;
}

// Different concrete types โ€” different sizes, different implementations
pub struct Circle { pub radius: f64 }
pub struct Rect { pub w: f64, pub h: f64 }

impl Drawable for Circle {
 fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
 fn perimeter(&self) -> f64 { 2.0 * std::f64::consts::PI * self.radius }
 fn name(&self) -> &'static str { "circle" }
}

// Heterogeneous collection โ€” concrete types are erased to Box<dyn Drawable>
pub struct Scene {
 shapes: Vec<Box<dyn Drawable>>,
}

impl Scene {
 pub fn add(mut self, s: impl Drawable + 'static) -> Self {
     self.shapes.push(Box::new(s));  // erases the type here
     self
 }

 pub fn total_area(&self) -> f64 {
     self.shapes.iter().map(|s| s.area()).sum()  // dispatch via vtable
 }
}
Erasing a value with a closure (mirrors OCaml GADT packing):
pub struct AnyShow(Box<dyn Showable>);

impl AnyShow {
 pub fn new<T: Showable + 'static>(v: T) -> Self {
     AnyShow(Box::new(v))  // T is erased โ€” only Showable remains
 }
}

// After construction, you can't recover T โ€” only call .show()
let items: Vec<AnyShow> = vec![
 AnyShow::new(42_i32),
 AnyShow::new("hello".to_string()),
 AnyShow::new(3.14_f64),
];
Erased callbacks (function type erasure):
pub struct Handler {
 callback: Box<dyn Fn(&str) -> String>,  // the function type is erased
}

// Any closure or function with the right signature works
let handlers = vec![
 Handler::new(|s| s.to_uppercase()),
 Handler::new(|s| s.chars().rev().collect()),
];

What This Unlocks

Key Differences

ConceptOCamlRust
Type erasure mechanismFirst-class modules (existential): `(module S : SHOW)``Box<dyn Trait>` โ€” trait object
DispatchFunction pointer through module recordvtable lookup โ€” fat pointer
AllocationModule packs value on heap`Box` heap-allocates the concrete value
Multi-capabilityModule with multiple valuesSuper-trait or combined trait bound
Recover concrete typePattern match GADT constructorNot directly โ€” use `Any::downcast_ref`
Zero-cost alternativeN/AGenerics with `impl Trait` (monomorphized)
// Type erasure: hide a concrete type behind a trait object.
// The concrete type is gone ("erased") at runtime; only the trait interface remains.
//
// OCaml uses first-class modules (existential types) for this.
// Rust uses `Box<dyn Trait>` (or `Arc<dyn Trait>` for shared ownership).

use std::fmt;

// โ”€โ”€ Showable: any value that can be displayed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub trait Showable {
    fn show(&self) -> String;
    fn type_name(&self) -> &'static str;
}

impl Showable for i32 {
    fn show(&self) -> String { self.to_string() }
    fn type_name(&self) -> &'static str { "i32" }
}

impl Showable for f64 {
    fn show(&self) -> String { format!("{}", self) }
    fn type_name(&self) -> &'static str { "f64" }
}

impl Showable for bool {
    fn show(&self) -> String { self.to_string() }
    fn type_name(&self) -> &'static str { "bool" }
}

impl Showable for String {
    fn show(&self) -> String { format!("\"{}\"", self) }
    fn type_name(&self) -> &'static str { "String" }
}

impl Showable for Vec<i32> {
    fn show(&self) -> String {
        let inner: Vec<String> = self.iter().map(|x| x.to_string()).collect();
        format!("[{}]", inner.join(", "))
    }
    fn type_name(&self) -> &'static str { "Vec<i32>" }
}

/// A type-erased showable value.
pub struct AnyShow(Box<dyn Showable>);

impl AnyShow {
    pub fn new<T: Showable + 'static>(v: T) -> Self {
        AnyShow(Box::new(v))
    }

    pub fn show(&self) -> String { self.0.show() }
    pub fn type_name(&self) -> &'static str { self.0.type_name() }
}

impl fmt::Display for AnyShow {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} ({})", self.show(), self.type_name())
    }
}

// โ”€โ”€ Drawable: shapes with area and perimeter โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub trait Drawable: fmt::Debug {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn name(&self) -> &'static str;
    fn describe(&self) -> String {
        format!("{}: area={:.2} perimeter={:.2}", self.name(), self.area(), self.perimeter())
    }
}

#[derive(Debug)]
pub struct Circle { pub radius: f64 }

#[derive(Debug)]
pub struct Rect { pub w: f64, pub h: f64 }

#[derive(Debug)]
pub struct Triangle { pub base: f64, pub height: f64, pub a: f64, pub b: f64, pub c: f64 }

impl Drawable for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
    fn perimeter(&self) -> f64 { 2.0 * std::f64::consts::PI * self.radius }
    fn name(&self) -> &'static str { "circle" }
}

impl Drawable for Rect {
    fn area(&self) -> f64 { self.w * self.h }
    fn perimeter(&self) -> f64 { 2.0 * (self.w + self.h) }
    fn name(&self) -> &'static str { "rectangle" }
}

impl Drawable for Triangle {
    fn area(&self) -> f64 { 0.5 * self.base * self.height }
    fn perimeter(&self) -> f64 { self.a + self.b + self.c }
    fn name(&self) -> &'static str { "triangle" }
}

/// A heterogeneous scene: different shape types, same interface.
pub struct Scene {
    shapes: Vec<Box<dyn Drawable>>,
}

impl Scene {
    pub fn new() -> Self { Scene { shapes: Vec::new() } }

    pub fn add(mut self, s: impl Drawable + 'static) -> Self {
        self.shapes.push(Box::new(s));
        self
    }

    pub fn total_area(&self) -> f64 {
        self.shapes.iter().map(|s| s.area()).sum()
    }

    pub fn describe_all(&self) {
        for s in &self.shapes {
            println!("  {}", s.describe());
        }
    }

    pub fn largest_area(&self) -> Option<&dyn Drawable> {
        self.shapes.iter()
            .max_by(|a, b| a.area().partial_cmp(&b.area()).unwrap())
            .map(|b| b.as_ref())
    }
}

// โ”€โ”€ Function-erased callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

pub struct Handler {
    callback: Box<dyn Fn(&str) -> String>,
}

impl Handler {
    pub fn new<F: Fn(&str) -> String + 'static>(f: F) -> Self {
        Handler { callback: Box::new(f) }
    }
    pub fn handle(&self, input: &str) -> String {
        (self.callback)(input)
    }
}

fn main() {
    // โ”€โ”€ Showable type erasure โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    let items: Vec<AnyShow> = vec![
        AnyShow::new(42_i32),
        AnyShow::new(3.14_f64),
        AnyShow::new(true),
        AnyShow::new("hello".to_string()),
        AnyShow::new(vec![1, 2, 3]),
    ];

    println!("Erased values:");
    for item in &items {
        println!("  {}", item);
    }

    // โ”€โ”€ Shape scene โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    let scene = Scene::new()
        .add(Circle { radius: 5.0 })
        .add(Rect { w: 3.0, h: 4.0 })
        .add(Triangle { base: 6.0, height: 4.0, a: 5.0, b: 5.0, c: 6.0 });

    println!("\nShapes:");
    scene.describe_all();
    println!("Total area: {:.2}", scene.total_area());
    if let Some(largest) = scene.largest_area() {
        println!("Largest: {}", largest.name());
    }

    // โ”€โ”€ Erased callbacks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    let handlers: Vec<Handler> = vec![
        Handler::new(|s| s.to_uppercase()),
        Handler::new(|s| s.chars().rev().collect()),
        Handler::new(|s| format!("len={}", s.len())),
    ];

    println!("\nHandlers on 'hello':");
    for h in &handlers {
        println!("  {}", h.handle("hello"));
    }
}

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

    #[test]
    fn test_any_show_i32() {
        let a = AnyShow::new(42_i32);
        assert_eq!(a.show(), "42");
        assert_eq!(a.type_name(), "i32");
    }

    #[test]
    fn test_any_show_string() {
        let a = AnyShow::new("hello".to_string());
        assert_eq!(a.show(), "\"hello\"");
    }

    #[test]
    fn test_scene_area() {
        let scene = Scene::new()
            .add(Rect { w: 4.0, h: 5.0 })
            .add(Rect { w: 2.0, h: 3.0 });
        assert!((scene.total_area() - 26.0).abs() < 0.001);
    }

    #[test]
    fn test_erased_shapes_hetero() {
        let scene = Scene::new()
            .add(Circle { radius: 1.0 })
            .add(Rect { w: 10.0, h: 10.0 });
        assert_eq!(scene.shapes.len(), 2);
        // Rect has area 100, Circle has area ~3.14 โ†’ largest is Rect
        assert_eq!(scene.largest_area().unwrap().name(), "rectangle");
    }

    #[test]
    fn test_handler_erasure() {
        let h = Handler::new(|s: &str| s.len().to_string());
        assert_eq!(h.handle("hello"), "5");
    }

    #[test]
    fn test_vec_showable() {
        let a = AnyShow::new(vec![1_i32, 2, 3]);
        assert_eq!(a.show(), "[1, 2, 3]");
    }
}
(* Type erasure via existential types (first-class modules in OCaml).
   Pack a value with its operations; the concrete type is hidden. *)

(* A "showable" value: we only know it can be converted to string *)
module type SHOWABLE = sig
  type t
  val value  : t
  val show   : t -> string
end

type showable = (module SHOWABLE)

let pack_int n : showable =
  (module struct
    type t = int
    let value = n
    let show = string_of_int
  end)

let pack_float f : showable =
  (module struct
    type t = float
    let value = f
    let show = string_of_float
  end)

let pack_bool b : showable =
  (module struct
    type t = bool
    let value = b
    let show = string_of_bool
  end)

let show_any (s : showable) =
  let module S = (val s) in
  S.show S.value

let () =
  let items = [
    pack_int 42;
    pack_float 3.14;
    pack_bool true;
    pack_int (-7);
  ] in
  Printf.printf "Erased values:\n";
  List.iter (fun s -> Printf.printf "  %s\n" (show_any s)) items

๐Ÿ“Š Detailed Comparison

Comparison: Type Erasure

OCaml

๐Ÿช Show OCaml equivalent
(* Record of closures *)
type shape = { draw : unit -> string; area : unit -> float }

let circle r = {
draw = (fun () -> Printf.sprintf "Circle(%.1f)" r);
area = (fun () -> Float.pi *. r *. r);
}

let shapes = [circle 5.0; rectangle 3.0 4.0]

Rust

// Trait object
trait Shape { fn draw(&self) -> String; fn area(&self) -> f64; }

let shapes: Vec<Box<dyn Shape>> = vec![
 Box::new(Circle { radius: 5.0 }),
 Box::new(Rectangle { width: 3.0, height: 4.0 }),
];