In this example, we will create a state machine for a simple traffic light system. The traffic light can be in one of three states: Red, Yellow, or Green. The state machine will transition between these states based on a set of defined events. We will also implement a method to display the current state of the traffic light.

Defining the States and Events

First, we need to define our states and events. In Rust, we can use enums to represent these.

#[derive(Debug)]
enum TrafficLightState {
    Red,
    Yellow,
    Green,
}

#[derive(Debug)]
enum TrafficLightEvent {
    TimerElapsed,
    Emergency,
}

Implementing the State Machine

Next, we will create a struct to represent our state machine. This struct will hold the current state and implement methods to handle events and transition between states.

struct TrafficLight {
    state: TrafficLightState,
}

impl TrafficLight {
    fn new() -> Self {
        TrafficLight {
            state: TrafficLightState::Red,
        }
    }

    fn handle_event(&mut self, event: TrafficLightEvent) {
        match (self.state, event) {
            (TrafficLightState::Red, TrafficLightEvent::TimerElapsed) => {
                self.state = TrafficLightState::Green;
            }
            (TrafficLightState::Green, TrafficLightEvent::TimerElapsed) => {
                self.state = TrafficLightState::Yellow;
            }
            (TrafficLightState::Yellow, TrafficLightEvent::TimerElapsed) => {
                self.state = TrafficLightState::Red;
            }
            (TrafficLightState::Red, TrafficLightEvent::Emergency) => {
                println!("Emergency! The light stays Red.");
            }
            (TrafficLightState::Green, TrafficLightEvent::Emergency) => {
                println!("Emergency! The light switches to Red.");
                self.state = TrafficLightState::Red;
            }
            (TrafficLightState::Yellow, TrafficLightEvent::Emergency) => {
                println!("Emergency! The light switches to Red.");
                self.state = TrafficLightState::Red;
            }
            _ => {}
        }
    }

    fn current_state(&self) -> &TrafficLightState {
        &self.state
    }
}

Using the State Machine

Now that we have our state machine implemented, we can create an instance of TrafficLight and simulate some events.

fn main() {
    let mut traffic_light = TrafficLight::new();

    println!("Initial state: {:?}", traffic_light.current_state());

    traffic_light.handle_event(TrafficLightEvent::TimerElapsed);
    println!("State after TimerElapsed: {:?}", traffic_light.current_state());

    traffic_light.handle_event(TrafficLightEvent::TimerElapsed);
    println!("State after TimerElapsed: {:?}", traffic_light.current_state());

    traffic_light.handle_event(TrafficLightEvent::TimerElapsed);
    println!("State after TimerElapsed: {:?}", traffic_light.current_state());

    traffic_light.handle_event(TrafficLightEvent::Emergency);
    println!("State after Emergency: {:?}", traffic_light.current_state());
}

Output Explanation

When you run the above code, the output will reflect the transitions of the traffic light based on the events:

Initial state: Red
State after TimerElapsed: Green
State after TimerElapsed: Yellow
State after TimerElapsed: Red
Emergency! The light stays Red.
State after Emergency: Red

Best Practices

  1. Use Enums for States and Events: Enums provide a clear and concise way to define possible states and events, making the code easier to read and maintain.
  1. Pattern Matching: Rust's pattern matching allows for elegant handling of state transitions. It makes it easy to define behavior based on the current state and the event received.
  1. Encapsulation: Encapsulate the state machine logic within a struct, which helps in maintaining the state and related methods together.
  1. Error Handling: Although not demonstrated here, consider how to handle unexpected events or states. You might want to log errors or panic under certain conditions.

Conclusion

In this tutorial, we demonstrated how to implement a simple state machine in Rust using enums and pattern matching. This pattern can be adapted for various applications beyond the traffic light example, such as game states, user interfaces, or protocol handling.

By following the best practices outlined, you can create robust and maintainable state machines in your Rust applications.

Learn more with useful resources