Info
This post is auto-generated from RSS feed The Rust Programming Language Forum - Latest topics. Source: Moving fields between states in a runtime state machine
Moving fields between states in a runtime state machine
โ Rust ๐ 2025-07-11 ๐ค surdeus ๐๏ธ 3Twice now, I've had to implement a state machine in Rust where transitions between states need to move data from the old state to the new, and I can't settle on a decent way to accomplish that. These state machines transition at runtime based on input received from outside the program, so all the search hits about implementing compile-time state machines in Rust are useless to me and just make it harder to find prior art.
As a simple and contrived example, let's consider implementing a sans IO state machine for the Request-O-Tron 12345. A typical session goes like this:
Welcome to the Request-O-Tron 12345! What is your name?
> Steve
What are you requesting, Steve?
> pen
What kind of pen, Steve?
> ballpoint
What color of ballpoint pen, Steve?
> blue
How many blue ballpoint pens, Steve?
> 1
Request successful! You will receive your 1 blue ballpoint pen(s) in 4 to 6 weeks. Thank you, Steve!
Naรฏvely, you might try to code this like so:
requestotron.rs (click for more details)Unfortunately, this fails to compile because handle_input()
tries to move String
fields out from behind a reference, and String
isn't Copy
.
So far, I've come up with the following possible workarounds:
Use std::mem::take()
on the fields. This only words when the field types all implement Default
.
Clone
the fields. This is usually a feasible option, but it wastes memory (unless the compiler is smart enough to just do a move instead? I have no idea how much that can be relied upon.).
Wrap the fields in Option
and use Option::take()
on them. This then forces you to use unwrap()
/expect()
at the end of processing or at any other time that you need access to the actual values, and impossible states are no longer unrepresentable.
Declare the fields on the machine struct itself, and change the state enum to purely C-style, like so:
struct RequestOTron {
message: Option<String>,
state: State,
name: String, // or Option<String>
request: String,
kind: String,
color: String,
}
enum State {
NeedName,
NeedRequest,
NeedKind,
NeedColor,
NeedQty,
Done,
}
This requires the fields to all have default/uninitialized values available which you have to be careful to avoid using before the actual initialization proper, and if any default is unsuitable for outside use, you've got the same problems as with Option
.
At the start of each method, use std::mem::replace()
to switch the state field with some "void" variant so that the state is no longer behind a reference, thereby allowing you to move fields out of the state and into the next variant, which you must ensure you put back by the end of the method. For the example above, this mostly consist of adding a State::Void
variant and changing match self.state
to match std::mem::replace(&mut self.state, State::Void)
. The main problem with this approach is that you'll need to be very careful around early returns in order to avoid leaving the "void" variant in place. The code can also get verbose if many inputs don't cause a state transition, forcing you to rebuild & assign back the state you just matched on.
This solution can be taken further by making each variant of the state enum into a unary tuple struct whose inner type holds the relevant fields and implements a handle_input(self, input: String) -> State
method (Note the consuming receiver!); combined with enum dispatch (whether through the crate of that name or just the general technique), this lets you simplify the handle_input()
method on the machine to just:
fn handle_input(&mut self, input: String) {
// Take out the state:
let state = std::mem::replace(&mut self.state, State::Void);
// Transition it:
let state = state.handle_input(input);
// And then put it back:
self.state = state;
}
โฆ while also having to write out a separate handle_input()
method on each of the new state types.
Now that I've written all this out, it seems that cloning and replace()
are the only decent options, so it comes down to excess memory usage (maybe) versus boilerplate code. (A lot probably comes down to that.)
Are there any other approaches to moving fields around that I've overlooked? What would you recommend or go with?
1 post - 1 participant
๐ท๏ธ rust_feed