Pretty State Machine Patterns in Rust (2016)

PaulHoule | 135 points

I'm a bit surprised that they don't use the name "type state". Perhaps it wasn't in wide use when this post was originally written?

The important ideas here are that each state is moved in to the method that transitions to the next state. This way you're "giving away" your ownership of the data. This is great for preventing errors. You cannot retain access to stale state.

And by using the From trait to implement transitions, you ensure that improper transitions are impossible to represent.

It's a great pattern and has only grown in use since this was written.

phibz | 20 days ago

I don't understand why you would code these explicit state machines when you can just write normal code that is much more readable. The state machine example they start with could be written as:

  while (true) {
     wait();
     fill();
     finish();
  }
I don't think the approach from the article would have any benefits like less bugs or higher performance.
gsliepen | 20 days ago

It’s funny seeing this blog post again. This is actually a reference I used to make a poker game as a state machine last year: https://github.com/theOGognf/private_poker

It made the development feel a lot safer and it’s nice knowing the poker game state cannot be illegally transitioned with the help of the type system

theOGognf | 20 days ago

Well this is timely :) I’m in the middle of writing a library, based on rust-fsm crate, that adds nice support for Mealy automata, with extensions like

- transition handlers

- guards

- clocks

- composition of automata into a system.

The idea is to allow write tooling that will export the automata into UPPAAL and allow for model checking. This way you don’t need to make too much additional effort to ensure your model and implementation match/are up to date, you can run the checker during CI tests to ensure you don’t have code that deadlocks/ some states are always reachable etc.

I plan to post a link here to HN once finished.

michalsustr | 20 days ago

Maybe I'm missing something, but it seems that this approach doesn't allow you to store external input that's provided when you transition states.

Say stateB is transitioned to from stateA and needs to store a value that is _not_ directly computed from stateA but is externally supplied at the point of transition.

As far as I understand this isn't possible with the proposed solution? Am I missing something? This seems like a pretty common use case to me.

mijoharas | 20 days ago

(2016) Popular, but barely discussed at the time (62 points, 3 comments) https://news.ycombinator.com/item?id=12703623

gnabgib | 20 days ago

This isn't really relevant to the article's topic, but I noticed something in their stylesheet that has me intensely curious--can anyone explain to me what's happening with this CSS rule?

  * {
 --index: calc(1 * var(--prime2) * var(--prime3) * var(--prime5) * var(--prime7) * var(--prime11) * var(--prime13) * var(--prime17) * var(--prime19) * var(--prime23) * var(--prime29) * var(--prime31) * var(--prime37) * var(--prime41) * var(--prime43) * var(--prime47) * var(--prime53) * var(--prime59) * var(--prime61) * var(--prime67) * var(--prime71) * var(--prime73) * var(--prime79) * var(--prime83) * var(--prime89) * var(--prime97) * var(--prime101) * var(--prime103) * var(--prime107) * var(--prime109) * var(--prime113) * var(--prime127) * var(--prime131) * var(--prime137) * var(--prime139) * var(--prime149) * var(--prime151) * var(--prime157) * var(--prime163) * var(--prime167) * var(--prime173) * var(--prime179) * var(--prime181) * var(--prime191) * var(--prime193) * var(--prime197) * var(--prime199) * var(--prime211) * var(--prime223) * var(--prime227) * var(--prime229) * var(--prime233) * var(--prime239) * var(--prime241) * var(--prime251) * var(--prime257) * var(--prime263) * var(--prime269) * var(--prime271) * var(--prime277) * var(--prime281) * var(--prime283) * var(--prime293) * var(--prime307) * var(--prime311) * var(--prime313) * var(--prime317) * var(--prime331) * var(--prime337) * var(--prime347) * var(--prime349) * var(--prime353) * var(--prime359) * var(--prime367) * var(--prime373) * var(--prime379) * var(--prime383) * var(--prime389) * var(--prime397) * var(--prime401) * var(--prime409) * var(--prime419) * var(--prime421) * var(--prime431) * var(--prime433) * var(--prime439) * var(--prime443) * var(--prime449) * var(--prime457) * var(--prime461) * var(--prime463) * var(--prime467) * var(--prime479) * var(--prime487) * var(--prime491) * var(--prime499) * var(--prime503) * var(--prime509) * var(--prime521) * var(--prime523) * var(--prime541) * var(--prime547) * var(--prime557) * var(--prime563) * var(--prime569) * var(--prime571) * var(--prime577) * var(--prime587) * var(--prime593) * var(--prime599));
}
nativeit | 19 days ago

i think stable coroutines [0] would be huge for rust. they would enable writing pure state machines in the form of straight line imperative code.

currently they’re used in the implementation of async/await, but aren’t themselves exposed.

[0]: https://doc.rust-lang.org/beta/unstable-book/language-featur...

skavi | 20 days ago

Is the title a nod to the Nine Inch Nails album "Pretty Hate Machine" ?

locusofself | 20 days ago

I prefer giving the transitions explicit names over relying on the From implemenations defined on the machine (defining them on the states still prevents bad transitions). The raft example drops a bunch of syntactic noise and repetition this way:

https://play.rust-lang.org/?version=stable&mode=debug&editio...

    fn main() {
        let is_follower = Raft::new(/* ... */);
        // Raft typically comes in groups of 3, 5, or 7. Just 1 for us. :)
        
        // Simulate this node timing out first.
        let is_candidate = is_follower.on_timeout();
        
        // It wins! How unexpected.
        let is_leader = is_candidate.on_wins_vote();
        
        // Then it fails and rejoins later, becoming a Follower again.
        let is_follower_again = is_leader.on_disconnected();
        
        // And goes up for election...
        let is_candidate_again = is_follower_again.on_timeout();
        
        // But this time it fails!
        let is_follower_another_time = is_candidate_again.on_lose_vote();
    }
    
    
    // This is our state machine.
    struct Raft<S> {
        // ... Shared Values
        state: S
    }
    
    // The three cluster states a Raft node can be in
    
    // If the node is the Leader of the cluster services requests and replicates its state.
    struct Leader {
        // ... Specific State Values
    }
    
    // If it is a Candidate it is attempting to become a leader due to timeout or initialization.
    struct Candidate {
        // ... Specific State Values
    }
    
    // Otherwise the node is a follower and is replicating state it receives.
    struct Follower {
        // ... Specific State Values
    }
    
    impl<S> Raft<S> {
        fn transition<T: From<S>>(self) -> Raft<T> {
            let state = self.state.into();
            // ... Logic prior to transition
            Raft {
                // ... attr: val.attr 
                state,
            }
        }
    }
    
    // Raft starts in the Follower state
    impl Raft<Follower> {
        fn new(/* ... */) -> Self {
            // ...
            Raft {
                // ...
                state: Follower { /* ... */ }
            }
        }
        
        // When a follower timeout triggers it begins to campaign
        fn on_timeout(self) -> Raft<Candidate> {
            self.transition()
        }
    }
    
    
    
    impl Raft<Candidate> {
        // If it doesn't receive a majority of votes it loses and becomes a follower again.
        fn on_lose_vote(self) -> Raft<Follower> {
            self.transition()
        }
    
        // If it wins it becomes the leader.
        fn on_wins_vote(self) -> Raft<Leader> {
            self.transition()
        }
    }
    
    impl Raft<Leader> {
        // If the leader becomes disconnected it may rejoin to discover it is no longer leader
        fn on_disconnected(self) -> Raft<Follower> {
            self.transition()
        }
    }
    
    // The following are the defined transitions between states.
    
    // When a follower timeout triggers it begins to campaign
    impl From<Follower> for Candidate {
        fn from(state: Follower) -> Self {
            Candidate { /* ... */ }
        }
    }
    
    // If it doesn't receive a majority of votes it loses and becomes a follower again.
    impl From<Candidate> for Follower {
        fn from(state: Candidate) -> Self {
            Follower { /* ... */ }
        }
    }
    
    // If it wins it becomes the leader.
    impl From<Candidate> for Leader {
        fn from(val: Candidate) -> Self {
            Leader { /* ... */ }
        }
    }
    
    // If the leader becomes disconnected it may rejoin to discover it is no longer leader
    impl From<Leader> for Follower {
        fn from(val: Leader) -> Self {
            Follower { /* ... */ }
        }
    }
joshka | 20 days ago

Is there any crate advised to be used when developing state machines? Any experience to share?

raphinou | 20 days ago

How about state machines with millions of transitions such as letter transducers?

jll29 | 19 days ago

Kind of interesting seeing folks rediscovering ideas from Standard ML.

pjmlp | 19 days ago

Honestly, I prefer the long-form first-cut that he dismisses to save 10 lines of code at the cost of using generics.

jesse__ | 20 days ago
[deleted]
| 19 days ago