I have some library code that consumes a value to produce another value. I want to abstract that code in my app with a state enum that I can mutate without consuming. Is that possible in safe Rust? How to do it idiomatically?
// Library code I can not change:
#[derive(Debug)]
struct One;
impl One {
pub fn two(self) -> Two {
Two
}
}
#[derive(Debug)]
struct Two;
// Application code I can change:
#[derive(Debug)]
enum State {
First(One),
Second(Two)
}
impl State {
pub fn new() -> Self {
Self::First(One)
}
pub fn transition(&mut self) -> () {
if let Self::First(one) = self {
// how to mutate self when new value requires comsumed field value?:
*self = Self::Second(one.two());
}
()
}
}
fn main() {
let mut state = State::new();
dbg!(&state);
state.transition();
dbg!(&state);
()
}
(Playground)
The issue with this is that if anything in the middle panics, self
will be dropped in an uninitialized state. There's mainly three options:
- Use one of
std::mem::{take, replace, swap}
to put a temporary value into self
.
- Make an unsafe guarantee that there are no panics.
- Catch panics and either prevent unwinding or replace the value at that point.
The last one is implemented in the replace_with crate.
2 Likes
The post from @drewtato describes the solutions. Here is an example of the first approach:
pub fn transition(&mut self) -> () {
let mut temp = State::First(one); // Any State will do
core::mem::swap(self, &mut temp);
if let Self::First(one) = temp {
temp = core::mem::replace(self, Self::Second(one.two()));
} else {
core::mem::swap(self, &mut temp);
}
}
Note that the temp
state construction is wasted and temp
is dropped at the end of the function. If construction of the state is expensive, you may be able to save temp
somewhere and reuse it to avoid that expense, but saving and accessing it may be impractical.
Another way is to add a State::InTransition
variant and use that for the temp state, since it is very cheap to construct. But then you need to handle that variant in all your matches on State, usually by panicking, which can be messy. And if State
is part of a public API, this would be undesirable.
3 Likes
Thanks for the explanation and further pointers. The RFC 1736 was a particularly insightful rabbit hole
I didn't think about unsoundness arising when the first value is taken out of the self reference, the library function already consumed it but panicked and didn't provide a replacement. In this case Rust can't guarantee the old value is still valid and has to either provide a new default value for replacement or panic/abort.
Yeah, I ultimately settled for an interim invalid state to explicitly model the possibility of a failure in transition.
2 Likes