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
- Zero-allocation heterogeneous collections โ store mixed concrete types in a `Vec<AnyAnimal>` with no heap per element.
- Plugin systems with known plugin sets โ define all plugin types at compile time, generate the dispatch enum, get fast dispatch with type safety.
- Hot path optimisation โ the compiler can inline the inner method calls through the `match` arms; `Box<dyn Trait>` cannot be inlined.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Heterogeneous collections | Polymorphic variants or GADT-based encoding | `dyn Trait` (heap) or enum dispatch (stack) |
| Dynamic dispatch | Objects / first-class modules | `Box<dyn Trait>` โ heap + vtable |
| Static enum dispatch | Variant matching is the norm | Enum dispatch macro generates match-based delegation |
| Adding a new type | New constructor + match arm | New variant + macro regenerates everything |
| Heap allocation | GC-managed; implicit | Explicit; 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