433: Macro-Defined State Machines
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
next() transition methods enforce valid state progressionmatches! simplifies guard conditions in state machine methodsCode 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
struct Door<S: DoorState>) making invalid transitions compile errors; OCaml can do this with GADTs but it's less common.match and OCaml match enforce exhaustive handling; both emit compiler warnings/errors for incomplete matches.next() takes self by value (returning new state); OCaml's transition functions return new state values functionally.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());
}
}#[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
Exercises
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.Start, InString, InNumber, InArray, Complete, Error. The next_char(c: char) transition method drives the machine.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.