ExamplesBy LevelBy TopicLearning Paths
433 Fundamental

433: Macro-Defined State Machines

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "433: Macro-Defined State Machines" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. State machines (finite automata) model systems with discrete states and transitions: TCP connection lifecycle, UI component states, traffic lights, parser automata. Key difference from OCaml: 1. **Type

Tutorial

The Problem

State machines (finite automata) model systems with discrete states and transitions: TCP connection lifecycle, UI component states, traffic lights, parser automata. Writing state machines by hand requires defining state enums, transition tables, and guards repeatedly. A state_machine! macro generates the enum, transition logic, and validity checks from a compact declaration. This keeps the state structure and transitions co-located and prevents invalid transitions from being written at all.

State machine macros appear in embedded systems (motor controllers, protocol implementations), parser generators, UI frameworks, and any system where invalid state transitions must be prevented.

🎯 Learning Outcomes

  • • Understand how enums naturally model finite state machine states
  • • Learn how next() transition methods enforce valid state progression
  • • See how macro-generated state machines prevent invalid transitions at compile time
  • • Understand the difference between enum-based (closed, compile-time) and table-based (open, runtime) state machines
  • • Learn how matches! simplifies guard conditions in state machine methods
  • Code Example

    #![allow(clippy::all)]
    //! State Machine Macro
    //!
    //! Defining state machines with macros.
    
    /// Simple state machine.
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum TrafficLight {
        Red,
        Yellow,
        Green,
    }
    
    impl TrafficLight {
        pub fn next(&self) -> TrafficLight {
            match self {
                TrafficLight::Red => TrafficLight::Green,
                TrafficLight::Green => TrafficLight::Yellow,
                TrafficLight::Yellow => TrafficLight::Red,
            }
        }
    }
    
    /// Door state machine.
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum DoorState {
        Open,
        Closed,
        Locked,
    }
    
    impl DoorState {
        pub fn can_open(&self) -> bool {
            matches!(self, DoorState::Closed)
        }
    
        pub fn can_close(&self) -> bool {
            matches!(self, DoorState::Open)
        }
    
        pub fn can_lock(&self) -> bool {
            matches!(self, DoorState::Closed)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_traffic_light_cycle() {
            let mut light = TrafficLight::Red;
            light = light.next(); // Green
            assert_eq!(light, TrafficLight::Green);
            light = light.next(); // Yellow
            assert_eq!(light, TrafficLight::Yellow);
            light = light.next(); // Red
            assert_eq!(light, TrafficLight::Red);
        }
    
        #[test]
        fn test_door_open() {
            let door = DoorState::Closed;
            assert!(door.can_open());
        }
    
        #[test]
        fn test_door_locked_cannot_open() {
            let door = DoorState::Locked;
            assert!(!door.can_open());
        }
    
        #[test]
        fn test_door_close() {
            let door = DoorState::Open;
            assert!(door.can_close());
        }
    
        #[test]
        fn test_door_lock() {
            let door = DoorState::Closed;
            assert!(door.can_lock());
        }
    }

    Key Differences

  • Type-state pattern: Rust can encode state in the type system (struct Door<S: DoorState>) making invalid transitions compile errors; OCaml can do this with GADTs but it's less common.
  • Exhaustiveness: Both Rust match and OCaml match enforce exhaustive handling; both emit compiler warnings/errors for incomplete matches.
  • Mutability: Rust's next() takes self by value (returning new state); OCaml's transition functions return new state values functionally.
  • Macro generation: Rust macros can generate state machine boilerplate from declarations; OCaml uses functor-based state machine libraries.
  • OCaml Approach

    OCaml state machines use algebraic types with pattern matching. type state = Red | Yellow | Green with let next = function Red -> Green | Green -> Yellow | Yellow -> Red. OCaml's exhaustiveness checking prevents forgetting a state in transitions. The with keyword in OCaml's record syntax enables expressing state updates cleanly. OCaml's module system can encapsulate the state machine behind an abstract type, hiding implementation details.

    Full Source

    #![allow(clippy::all)]
    //! State Machine Macro
    //!
    //! Defining state machines with macros.
    
    /// Simple state machine.
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum TrafficLight {
        Red,
        Yellow,
        Green,
    }
    
    impl TrafficLight {
        pub fn next(&self) -> TrafficLight {
            match self {
                TrafficLight::Red => TrafficLight::Green,
                TrafficLight::Green => TrafficLight::Yellow,
                TrafficLight::Yellow => TrafficLight::Red,
            }
        }
    }
    
    /// Door state machine.
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum DoorState {
        Open,
        Closed,
        Locked,
    }
    
    impl DoorState {
        pub fn can_open(&self) -> bool {
            matches!(self, DoorState::Closed)
        }
    
        pub fn can_close(&self) -> bool {
            matches!(self, DoorState::Open)
        }
    
        pub fn can_lock(&self) -> bool {
            matches!(self, DoorState::Closed)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_traffic_light_cycle() {
            let mut light = TrafficLight::Red;
            light = light.next(); // Green
            assert_eq!(light, TrafficLight::Green);
            light = light.next(); // Yellow
            assert_eq!(light, TrafficLight::Yellow);
            light = light.next(); // Red
            assert_eq!(light, TrafficLight::Red);
        }
    
        #[test]
        fn test_door_open() {
            let door = DoorState::Closed;
            assert!(door.can_open());
        }
    
        #[test]
        fn test_door_locked_cannot_open() {
            let door = DoorState::Locked;
            assert!(!door.can_open());
        }
    
        #[test]
        fn test_door_close() {
            let door = DoorState::Open;
            assert!(door.can_close());
        }
    
        #[test]
        fn test_door_lock() {
            let door = DoorState::Closed;
            assert!(door.can_lock());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_traffic_light_cycle() {
            let mut light = TrafficLight::Red;
            light = light.next(); // Green
            assert_eq!(light, TrafficLight::Green);
            light = light.next(); // Yellow
            assert_eq!(light, TrafficLight::Yellow);
            light = light.next(); // Red
            assert_eq!(light, TrafficLight::Red);
        }
    
        #[test]
        fn test_door_open() {
            let door = DoorState::Closed;
            assert!(door.can_open());
        }
    
        #[test]
        fn test_door_locked_cannot_open() {
            let door = DoorState::Locked;
            assert!(!door.can_open());
        }
    
        #[test]
        fn test_door_close() {
            let door = DoorState::Open;
            assert!(door.can_close());
        }
    
        #[test]
        fn test_door_lock() {
            let door = DoorState::Closed;
            assert!(door.can_lock());
        }
    }

    Deep Comparison

    OCaml vs Rust: macro state machine

    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

  • Type-state door: Implement Door<S> where S is a phantom type parameter (struct Open; struct Closed; struct Locked). Write fn open(door: Door<Closed>) -> Door<Open> and fn lock(door: Door<Closed>) -> Door<Locked>. Prove that fn open(door: Door<Locked>) doesn't compile.
  • Parser state machine: Implement a simple JSON token parser as a state machine with states Start, InString, InNumber, InArray, Complete, Error. The next_char(c: char) transition method drives the machine.
  • State machine macro: Write state_machine!(Light { Red --trigger:Timer--> Green, Green --trigger:Timer--> Yellow, Yellow --trigger:Timer--> Red }) that generates the enum, a trigger enum, and a transition(event: Trigger) -> Option<Self> method.
  • Open Source Repos