State machine ownership awkwardness

#1

Hello!

I’ve been trying out some async programming with tokio, and as such am now trying to find a comfortable pattern for state machine programming. I’ve read the “Pretty State Machine Patterns” blog post, which I like, but I’d like to manually work up to that level of sophistication.

In implementing what I thought would be a simple enum-based state machine, I ran into a conundrum. When one of my enum variants had some payload which needed to be moved along into the next state, I ran into an awkward ownership situation. The following code is basically what I’m trying to do, but I can’t figure out how to move my payload from one state enum to the next, because self is already borrowed as part of the function call.

Any thoughts would be welcome!

struct CantMoveMe {
    // doesn't matter
}

enum State {
    Working(CantMoveMe),
    Done(CantMoveMe)
}

struct StateMachine(State);

impl StateMachine {
    fn finish(&mut self) {
        if let State::Working(payload) = self.0 {
            self.0 = State::Done(payload);
        }
    }
}

fn main() {
    let mut machine = StateMachine(State::Working(CantMoveMe{}));
    machine.finish();
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0507]: cannot move out of borrowed content
  --> src/main.rs:14:42
   |
14 |         if let State::Working(payload) = self.0 {
   |                               -------    ^^^^^^
   |                               |          |
   |                               |          cannot move out of borrowed content
   |                               |          help: consider borrowing here: `&self.0`
   |                               data moved here
   |
note: move occurs because `payload` has type `CantMoveMe`, which does not implement the `Copy` trait
  --> src/main.rs:14:31
   |
14 |         if let State::Working(payload) = self.0 {
   |                               ^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0507`.
error: Could not compile `playground`.

To learn more, run the command again with --verbose.

0 Likes

#2

There are basically two options here.

  1. Have a “dummy” state variant that you set your state machine storage to (using mem::replace) while doing the state update. Or, equivalently, wrap your State in an option:
struct StateMachine(Option<State>);

...

        if let State::Working(payload) = self.0.take().unwrap() {
            self.0 = Some(State::Done(payload));
        }
  1. Take your state by-value. If you write fn finish(self) -> Self you don’t run into these borrowing issues. (There are crates like https://crates.io/crates/take_mut and https://crates.io/crates/replace_with that can do this using a &mut T but their use seems… dangerous.)
2 Likes

#4

Thanks for the suggestions!

The option one is not my favorite, as the reason I’m using the enum in the first place is to make sure the state machine is in a valid state at all times, via the type system. I’ll see about the mem::replace option. Maybe that’ll work, but that still leaves my state machine in an intermediate (if not exactly invalid) state for some period.

Your #2 option is maybe the right way to go. I initially tried it this way, but it ended up being a bit awkward interfacing that way with tokio. I’ll try again — I bet I can make it work!

Thanks again.

2 Likes

#5

FWIW I’m regularly using option #2 and I quite like it.
I’ve been modeling mines after this blog post: https://hoverbear.org/2016/10/12/rust-state-machine-pattern/

0 Likes

#6

Yep, once I figured out that my tokio stream object should be using a fold() rather than a for_each() to manage the progression of the states, everything fell into place quite nicely! Thanks for the help!

1 Like

#7

That’s the core of the problem — you wish to make your data invalid for a period of time and the compiler is preventing you from doing so.

if let State::Working(payload) = self.0 {
    // panic here
    self.0 = State::Done(payload);

If a panic were to occur after you have removed the value from self.0 but before you’ve put a new value back, then the destructor of self could access uninitialized memory, causing memory unsafety.

See also:

6 Likes

#8

std::mem::replace is a neat interface, which I didn’t know about before! Thanks for the links!

The solution I’m working with is a fold over the tokio stream, with the accumulated value being the state enum. That seems to work great for my purposes.

1 Like

#9

Right, which is the “take your state by-value” version that @jethrogb and some of the SO answers encourage.

My comment was more to point out why your original code can never work / why it needs some type of placeholder value (even if that’s via Option).

1 Like

#10

Got it! :+1:

0 Likes