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.
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.
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));
}
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!
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!
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.
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.