Can I abstract value consumption behind mutation?

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 :smiley: 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