DEV Community

Cover image for Designing state machines in Rust
senyo
senyo

Posted on • Edited on

12

Designing state machines in Rust

This post was originally posted on my blog


In Rust, you often hear about state machines. Futures are state machines! I thought it would be cool to read more about it. I came across this blog post (funnily enough, by a friend and mentor of mine) which really helped me! I highly recommend reading it. In this post, I'm just noting the part I found relevant. The example here is from her blog post.

P.S.A: Go and read that post 😆

State Machines

Rust is able to represent state machines quite elegantly. Due to its type system, you can make sure invariants are upheld. To use a real life example, we can do some basic modeling of Raft (no knowledge needed for this post). A node in a Raft cluster can be in one of 3 states:

  • Candidate
  • Follower
  • Leader

The possible transitions are:

  • Follower -> Candidate
  • Candidate -> Leader
  • Candidate -> Follower
  • Leader -> Follower

Therefore the only invalid state transition is: Follower -> Leader.

Now we can start modeling this. We have a struct Node that represents a node in the cluster

struct Node<S> {
  ...
  state: S
}
Enter fullscreen mode Exit fullscreen mode

S is a generic parameter over the possible states. Our states are also structs

struct Follower; // In reality, these are likely to hold some data
struct Candidate;
struct Leader;
Enter fullscreen mode Exit fullscreen mode

In Raft, all nodes start in the Follower state. We can define a function new for that state only.

impl Node<Follower> {
  pub fn new() -> Node<Follower> {
    ..
    state: Follower {}
  }
}
Enter fullscreen mode Exit fullscreen mode

We can initialise this like so

let follower = Node::<Follower>::new();
Enter fullscreen mode Exit fullscreen mode

The important thing is that we can't do this for other states! If we do

let leader = Node::<Leader>::new();
Enter fullscreen mode Exit fullscreen mode

we will get an error because we haven't defined the function new for Node<Leader>. This is super sweet!

The next thing we need to be able to do is have state transitions enforceable by the compiler. We can do this using the From trait. If we want to transition from Follower to Candidate we can implement From for it.

impl From<Node<Follower>> for Node<Candidate> {
  fn from(state: Node<Follower>) -> Node<Candidate> {
    let candidate = Node {
      ..
      state: Candidate {}
    }
    candidate
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use it as such

let follower = Node { state: Follower {}}
let candidate = Node::<Candidate>::from(follower);
Enter fullscreen mode Exit fullscreen mode

Now we've gotten the type system on our side! If we don't implement From for Follower -> Leader, doing

let leader = Node::<Leader>::from(follower);
Enter fullscreen mode Exit fullscreen mode

will cause a compilation error. Nice!

Thoughts

On using this design, one issue I came across was implementing general behaviours across all states. Since each impl block is dedicated to a particular state (e.g Node<Follower>, Node<Leader>), you have to duplicate any methods you use across them. We can get around this using our favourite feature: traits! We could have something like

struct Follower;
struct Candidate;
struct Leader;

trait NodeState {}
impl NodeState for Follower {}
impl NodeState for Candidate {}
impl NodeState for Leader {}

impl <S: NodeState> Node<S> {
    // Generic methods
}
Enter fullscreen mode Exit fullscreen mode

We've added the trait bound NodeState. All states S that implement NodeState will have the functionality in the impl block.

Heroku

Simplify your DevOps and maximize your time.

Since 2007, Heroku has been the go-to platform for developers as it monitors uptime, performance, and infrastructure concerns, allowing you to focus on writing code.

Learn More

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay