๐Ÿฆ€ Functional Rust

432: enum_dispatch via Macros

Difficulty: 4 Level: Expert Generate a closed-set enum that delegates trait method calls to its variants โ€” the same performance as a hand-written `match`, with none of the boilerplate, and zero heap allocation.

The Problem This Solves

When you have a trait and several types that implement it, the idiomatic way to store them heterogeneously is `Box<dyn Trait>` โ€” a fat pointer with heap allocation and a virtual dispatch table. For most use cases that's fine. But in hot loops, plugin systems, or game ECS code, the heap allocation and cache-unfriendly vtable lookup matter. You want static dispatch but still need to store mixed types in a `Vec`. The manual solution is to write an enum with one variant per concrete type and implement the trait on the enum by delegating each method to the inner value via `match`. This is fast โ€” the compiler can inline the inner calls โ€” but it's massive boilerplate. Adding a new type means adding a variant, an `impl From`, and touching every method arm. The `enum_dispatch` macro automates this pattern: you declare the trait, the enum, and the types, and the macro generates the full delegation impl and all `From` conversions.

The Intuition

The generated enum is an ordinary Rust enum โ€” it lives on the stack, its size is the max of its variants. The macro writes what you'd write by hand: a `match` arm for each variant that calls the same method on the inner value. This is the "closed world" trade-off: you give up the ability to add new types at runtime (unlike `dyn Trait`) in exchange for zero heap allocation, inlineable dispatch, and the compiler knowing the full set of possibilities (enabling exhaustiveness checks and optimisations).

How It Works in Rust

// The macro generates: enum, trait impl with match delegation, From impls
macro_rules! enum_dispatch {
 (
     trait $trait_name:ident { $(fn $method:ident($($p:ident: $t:ty),*) -> $ret:ty;)* }
     enum $enum_name:ident { $($variant:ident($inner:ty)),* $(,)? }
 ) => {
     #[derive(Debug)]
     enum $enum_name { $($variant($inner),)* }

     impl $trait_name for $enum_name {
         $(fn $method(&self, $($p: $t),*) -> $ret {
             match self {
                 $($enum_name::$variant(inner) => inner.$method($($p),*),)*
             }
         })*
     }

     $(impl From<$inner> for $enum_name {
         fn from(x: $inner) -> Self { $enum_name::$variant(x) }
     })*
 };
}

trait Animal {
 fn speak(&self) -> String;
 fn speed(&self) -> f64;
}

struct Dog; struct Cat;
impl Animal for Dog { fn speak(&self) -> String { "Woof!".into() } fn speed(&self) -> f64 { 5.0 } }
impl Animal for Cat { fn speak(&self) -> String { "Meow!".into() } fn speed(&self) -> f64 { 8.0 } }

// Macro invocation โ€” generates AnyAnimal enum + impl Animal for AnyAnimal
enum_dispatch! {
 trait Animal { fn speak() -> String; fn speed() -> f64; }
 enum AnyAnimal { Dog(Dog), Cat(Cat), }
}

// Usage: no Box, no heap, stored by value
let animals: Vec<AnyAnimal> = vec![Dog.into(), Cat.into()];
for a in &animals {
 println!("{} ({}m/s)", a.speak(), a.speed()); // static dispatch via match
}

// Size: the enum, not a pointer
println!("{}", std::mem::size_of::<AnyAnimal>());   // e.g. 24
println!("{}", std::mem::size_of::<Box<dyn Animal>>()); // 16 (pointer + vtable ptr)
What the macro generates (expanded):
enum AnyAnimal { Dog(Dog), Cat(Cat) }
impl Animal for AnyAnimal {
 fn speak(&self) -> String {
     match self {
         AnyAnimal::Dog(inner) => inner.speak(),
         AnyAnimal::Cat(inner) => inner.speak(),
     }
 }
 fn speed(&self) -> f64 {
     match self { AnyAnimal::Dog(i) => i.speed(), AnyAnimal::Cat(i) => i.speed() }
 }
}
impl From<Dog> for AnyAnimal { fn from(x: Dog) -> Self { AnyAnimal::Dog(x) } }
impl From<Cat> for AnyAnimal { fn from(x: Cat) -> Self { AnyAnimal::Cat(x) } }

What This Unlocks

Key Differences

ConceptOCamlRust
Heterogeneous collectionsPolymorphic variants or GADT-based encoding`dyn Trait` (heap) or enum dispatch (stack)
Dynamic dispatchObjects / first-class modules`Box<dyn Trait>` โ€” heap + vtable
Static enum dispatchVariant matching is the normEnum dispatch macro generates match-based delegation
Adding a new typeNew constructor + match armNew variant + macro regenerates everything
Heap allocationGC-managed; implicitExplicit; enum dispatch avoids it entirely
// enum_dispatch via macros in Rust
// Eliminates dyn Trait overhead by converting to static dispatch

// The macro generates:
// 1. An enum with one variant per type
// 2. A trait impl for the enum that delegates via match

macro_rules! enum_dispatch {
    (
        trait $trait_name:ident {
            $( fn $method:ident ( $($param:ident : $pty:ty),* ) -> $ret:ty ; )*
        }
        enum $enum_name:ident {
            $( $variant:ident ( $inner_ty:ty ) ),* $(,)?
        }
    ) => {
        // The enum
        #[derive(Debug)]
        enum $enum_name {
            $( $variant($inner_ty), )*
        }

        // Trait impl: delegate each method to the inner type
        impl $trait_name for $enum_name {
            $(
                fn $method ( &self, $($param: $pty),* ) -> $ret {
                    match self {
                        $( $enum_name::$variant(inner) => inner.$method($($param),*), )*
                    }
                }
            )*
        }

        // From impls for each variant
        $(
            impl From<$inner_ty> for $enum_name {
                fn from(x: $inner_ty) -> Self { $enum_name::$variant(x) }
            }
        )*
    };
}

// Define the trait
trait Animal {
    fn speak(&self) -> String;
    fn name(&self) -> String;
    fn speed(&self) -> f64;
}

// Concrete types
struct Dog { name: String }
struct Cat { name: String }
struct Bird { name: String, speed: f64 }

impl Animal for Dog {
    fn speak(&self) -> String { "Woof!".into() }
    fn name(&self) -> String { self.name.clone() }
    fn speed(&self) -> f64 { 5.0 }
}

impl Animal for Cat {
    fn speak(&self) -> String { "Meow!".into() }
    fn name(&self) -> String { self.name.clone() }
    fn speed(&self) -> f64 { 8.0 }
}

impl Animal for Bird {
    fn speak(&self) -> String { "Tweet!".into() }
    fn name(&self) -> String { self.name.clone() }
    fn speed(&self) -> f64 { self.speed }
}

// Generate the enum dispatch
enum_dispatch! {
    trait Animal {
        fn speak() -> String;
        fn name() -> String;
        fn speed() -> f64;
    }
    enum AnyAnimal {
        Dog(Dog),
        Cat(Cat),
        Bird(Bird),
    }
}

fn describe(a: &AnyAnimal) {
    println!("{} says: {} (speed: {})", a.name(), a.speak(), a.speed());
}

fn main() {
    let animals: Vec<AnyAnimal> = vec![
        Dog { name: "Rex".to_string() }.into(),
        Cat { name: "Whiskers".to_string() }.into(),
        Bird { name: "Tweety".to_string(), speed: 30.0 }.into(),
    ];

    for a in &animals {
        describe(a);
    }

    // No heap allocation! Stored by value
    println!("\nSize of AnyAnimal: {} bytes", std::mem::size_of::<AnyAnimal>());
    println!("Size of Box<dyn Animal> (for comparison): {} bytes",
             std::mem::size_of::<Box<dyn Animal>>());
}

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

    #[test]
    fn test_enum_dispatch() {
        let d: AnyAnimal = Dog { name: "Buddy".to_string() }.into();
        assert_eq!(d.speak(), "Woof!");
        assert_eq!(d.name(), "Buddy");
    }

    #[test]
    fn test_no_heap_alloc() {
        // Stack allocated, not heap
        let _a: AnyAnimal = Cat { name: "Felix".to_string() }.into();
        // No Box needed
    }
}
(* enum_dispatch via macros in OCaml *)

(* OCaml algebraic types already achieve this pattern natively *)
type animal =
  | Dog of string  (* name *)
  | Cat of string
  | Bird of string * bool  (* name, can_fly *)

(* This IS "enum dispatch" โ€” pattern match is the dispatch mechanism *)
let speak = function
  | Dog _ -> "Woof!"
  | Cat _ -> "Meow!"
  | Bird _ -> "Tweet!"

let name = function
  | Dog n | Cat n | Bird (n, _) -> n

let can_fly = function
  | Bird (_, f) -> f
  | _ -> false

let describe a =
  Printf.printf "%s says: %s (flies: %b)\n"
    (name a) (speak a) (can_fly a)

let () =
  let animals = [Dog "Rex"; Cat "Whiskers"; Bird ("Tweety", true)] in
  List.iter describe animals