ExamplesBy LevelBy TopicLearning Paths
432 Fundamental

432: Macro Enum Dispatch

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "432: Macro Enum Dispatch" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Enums with many variants often need repetitive match arms for dispatch methods. Key difference from OCaml: 1. **Exhaustiveness**: Both Rust and OCaml require exhaustive match arms; adding a variant to a `dispatch_enum!`

Tutorial

The Problem

Enums with many variants often need repetitive match arms for dispatch methods. A Command enum with 20 variants requires a 20-arm match for each dispatch method. When variants map uniformly to trait implementations, macros can generate all the match arms automatically. This is the enum_dispatch crate's core idea: eliminate the boilerplate of matching each variant and delegating to the inner type. The dispatch_enum! macro generates both the enum and its dispatch methods from a compact declaration.

Enum dispatch macros appear in event handling systems, command patterns, codec implementations, and any state machine with many uniform variant behaviors.

🎯 Learning Outcomes

  • • Understand how dispatch_enum! generates both enum variants and dispatch methods
  • • Learn how stringify!($variant) in match arms provides zero-cost variant names
  • • See how macros reduce the O(n) boilerplate of adding dispatch methods to enums
  • • Understand when enum dispatch (closed set, fast) is better than dyn Trait (open set, flexible)
  • • Learn how the matches! macro simplifies variant-checking predicates
  • Code Example

    #![allow(clippy::all)]
    //! Enum Dispatch Macro
    //!
    //! Generating match arms for enums.
    
    /// Define enum with dispatch method.
    #[macro_export]
    macro_rules! dispatch_enum {
        ($name:ident { $($variant:ident),* $(,)? }) => {
            #[derive(Debug, Clone, Copy)]
            pub enum $name { $($variant,)* }
    
            impl $name {
                pub fn name(&self) -> &'static str {
                    match self {
                        $(Self::$variant => stringify!($variant),)*
                    }
                }
            }
        };
    }
    
    dispatch_enum!(Action {
        Start,
        Stop,
        Pause,
        Resume
    });
    
    /// Command pattern with enum.
    pub enum Command {
        Print(String),
        Add(i32, i32),
        Exit,
    }
    
    impl Command {
        pub fn execute(&self) -> String {
            match self {
                Command::Print(s) => format!("Print: {}", s),
                Command::Add(a, b) => format!("Add: {} + {} = {}", a, b, a + b),
                Command::Exit => "Exit".to_string(),
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_dispatch_name() {
            assert_eq!(Action::Start.name(), "Start");
            assert_eq!(Action::Stop.name(), "Stop");
        }
    
        #[test]
        fn test_command_print() {
            let c = Command::Print("Hello".into());
            assert!(c.execute().contains("Hello"));
        }
    
        #[test]
        fn test_command_add() {
            let c = Command::Add(2, 3);
            assert!(c.execute().contains("5"));
        }
    
        #[test]
        fn test_command_exit() {
            let c = Command::Exit;
            assert_eq!(c.execute(), "Exit");
        }
    
        #[test]
        fn test_all_actions() {
            let actions = [Action::Start, Action::Stop, Action::Pause, Action::Resume];
            assert_eq!(actions.len(), 4);
        }
    }

    Key Differences

  • Exhaustiveness: Both Rust and OCaml require exhaustive match arms; adding a variant to a dispatch_enum!-generated enum requires updating all dispatch methods.
  • Name generation: Rust's stringify!($variant) gives the variant name as a string automatically; OCaml needs explicit string mappings or deriving.
  • Macro vs. pattern: OCaml's pattern matching is a language feature; Rust's dispatch is a macro-generated impl.
  • Closed vs. open: Both enum dispatch approaches are closed sets; dyn Trait in Rust and first-class modules in OCaml handle open sets.
  • OCaml Approach

    OCaml handles enum dispatch through algebraic types and pattern matching. A type command = Print of string | Add of int * int | Exit with let execute = function Print s -> ... | Add (a,b) -> ... | Exit -> ... is idiomatic. OCaml's exhaustiveness checking ensures all variants are handled. Adding a new variant breaks existing match expressions — a feature (forces updates) or a bug (breaks compatibility).

    Full Source

    #![allow(clippy::all)]
    //! Enum Dispatch Macro
    //!
    //! Generating match arms for enums.
    
    /// Define enum with dispatch method.
    #[macro_export]
    macro_rules! dispatch_enum {
        ($name:ident { $($variant:ident),* $(,)? }) => {
            #[derive(Debug, Clone, Copy)]
            pub enum $name { $($variant,)* }
    
            impl $name {
                pub fn name(&self) -> &'static str {
                    match self {
                        $(Self::$variant => stringify!($variant),)*
                    }
                }
            }
        };
    }
    
    dispatch_enum!(Action {
        Start,
        Stop,
        Pause,
        Resume
    });
    
    /// Command pattern with enum.
    pub enum Command {
        Print(String),
        Add(i32, i32),
        Exit,
    }
    
    impl Command {
        pub fn execute(&self) -> String {
            match self {
                Command::Print(s) => format!("Print: {}", s),
                Command::Add(a, b) => format!("Add: {} + {} = {}", a, b, a + b),
                Command::Exit => "Exit".to_string(),
            }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_dispatch_name() {
            assert_eq!(Action::Start.name(), "Start");
            assert_eq!(Action::Stop.name(), "Stop");
        }
    
        #[test]
        fn test_command_print() {
            let c = Command::Print("Hello".into());
            assert!(c.execute().contains("Hello"));
        }
    
        #[test]
        fn test_command_add() {
            let c = Command::Add(2, 3);
            assert!(c.execute().contains("5"));
        }
    
        #[test]
        fn test_command_exit() {
            let c = Command::Exit;
            assert_eq!(c.execute(), "Exit");
        }
    
        #[test]
        fn test_all_actions() {
            let actions = [Action::Start, Action::Stop, Action::Pause, Action::Resume];
            assert_eq!(actions.len(), 4);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_dispatch_name() {
            assert_eq!(Action::Start.name(), "Start");
            assert_eq!(Action::Stop.name(), "Stop");
        }
    
        #[test]
        fn test_command_print() {
            let c = Command::Print("Hello".into());
            assert!(c.execute().contains("Hello"));
        }
    
        #[test]
        fn test_command_add() {
            let c = Command::Add(2, 3);
            assert!(c.execute().contains("5"));
        }
    
        #[test]
        fn test_command_exit() {
            let c = Command::Exit;
            assert_eq!(c.execute(), "Exit");
        }
    
        #[test]
        fn test_all_actions() {
            let actions = [Action::Start, Action::Stop, Action::Pause, Action::Resume];
            assert_eq!(actions.len(), 4);
        }
    }

    Deep Comparison

    OCaml vs Rust: macro enum dispatch

    See example.rs and example.ml for side-by-side implementations.

    Key Points

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Codec dispatch: Use dispatch_enum! to generate a Format { Json, Csv, Toml, Binary } enum with a content_type() -> &'static str method returning the MIME type for each format.
  • Error category: Create ErrorCategory { Network, Io, Parse, Permission, Timeout } with dispatch methods is_retryable() -> bool and log_level() -> &'static str. Implement these with different behavior per variant.
  • Dispatch with data: Extend the pattern to handle variants with payloads: dispatch_data!( Message { Text(String) => fn len -> String::len(data), Binary(Vec<u8>) => fn len -> data.len() } ).
  • Open Source Repos