Rust State Machine Part 1
tldr
- Define the states.
struct Aand optionallyState<S>for type safe callsenumto hold the structs
- Define the commands so it could be “send” by the client
- Define the control panel which has a sender tx
- Spawn the control panel
- initial
current_stateinOption while let Ok(cmd) = rx.recv()current_state = Some(match(state, cmd){});
- initial
- Full example code here
Overview
I’ve been learning how to write a state machine in Rust and I am at a point where I could stop and write something down.
I would be starting from a very simple one then go to one which allows multiple threads to try access the same state machine.
Note that the code is not production ready, it’s more of a study note post.
There would be a part 2 once I learned how to handle two-way communications and work with tokio.
The Scenario
Say we want to model states of traffic lights. It only has 3 states. Red, Green and Yellow.
stateDiagram-v2
[*] --> Red
Red --> Green
Green --> Yellow
Yellow --> Red
Simple Traffic Light
Here let’s write some code to model the state machine above.
|
|
The above defines three empty structs and implements their transitions.
A few things worth noting.
- Only
Redhaspub fn new() -> Self, this is because our state machine can only start fromRed(The code in this example actually allows constructing directly, let’s ignore that for the sake of this post) - Each of the
to_xxxtakes inself, meaning it is actually consumingselfinstead of borrowing it. For instance when we callto_green(self) -> Green, the existing state is consumed and a newGreenstate is created.
The benefit of this structure is that the state machine is enforced by the rust compiler.
The output of the above is
start with red
red to green
green to yellow
yellow to red
Honestly this isn’t really that impressive. Also this might be fine to be used within a single thread but what if it needs to be shared by multiple threads? This might be a nightmare since all the state transitions require consuming the state.
There is an elegant way to handle this. It’s called the actor pattern. Let’s have a look at it.
Traffic Light Remote Control
Assume that I want to create a traffic light remote control, where on the remote control I can press buttons to change the light’s color. And not only I have the control, my friends could have their own control but we all control the same traffic light.
There are 4 steps, and I’ll go over them one by one
Step 1: Define The Physical States
We would still be using the empty structs but on top of that we need to create an enum that wraps over them. The reason an enum is needed is because we needed a “box” to hold the states. You’ll see this later.
|
|
Note that in pub enum TrafficLight, Red(Red) reads as “Defining an enum called Red which wraps around state::Red”.
Step 2: Define The Commands
We need to define the commands our remote control would send, these would be defined as an enum.
Step 3: Define The Control Panel (The Remote Control)
Let’s define the struct that clients would use to send the commands.
|
|
Ignore the unwrap() here, the idea is that TrafficLightRemote holds a mpsc::Sender<Command> and can send commands to attempt to modify the state.
Step 4: Spawn The Control Panel (The Remote Control)
This is where we match the current state and command then decide what to do. Let’s have a look at the code and I’ll point out a few important things.
|
|
while let Ok(cmd) = rx.recv()would exit when all senders are dropped.- The last
(original_state, _bad_cmd)match is needed to satisfy exhaustiveness - If
(_, _)was used instead of(original_state, _bad_cmd), thenstatewould actually be dropped, socurrent_statewould either be()which triggers a compile error or I forceNonein it which would break the next command. greenin(TrafficLight::Green(green), Command::ToYellow)is of typestruct Green. This is where the type safe calls is enforced.current_statehas to bemutbecause it’s being reassigned atcurrent_state = Some(match (state, cmd) {...});movemovesrxin the thread- When calling
client.clone()only the sender is cloned, therxlives in one and only one thread
What is missing?
See the following main.rs, it sometimes does not print because the main thread exits so the client thread never got the chance to print out the messages. A sleep was added just to demo the print behavior.
Two way communication is one way to fix this. I’ll go and learn it then write part 2.
See Also
- Map Flatmap and Options
- Rust Learning: Modules
- Rust Polars Parquet
- Rust Learning: Deref
- Rust Learning: Lifetime