Search code examples
rustownership

Rebind a variable to a different type while used in the `match` declaration


I am writing Rust code with a typestate pattern to create an API and a CLI for it.

I have a struct VendingMachine<S> where S is a ZST representing my current state (e.g. VendingMachine<Start>,VendingMachine<Payment>,VendingMachine<Checkout> etc.)

I want to have my vending machine to run in a loop and match each state to some code:

let mut machine = VendingMachine::new::<Start>(/*...*/);
loop {
    match machine.state {
        Start => {/* do something and then change the type of `machine` to VendingMachine<Payment> using some transitioning method */},
        Payment => {/* do something else and then change the type of `machine` to VendingMachine<Checkout> using some other transitioning method */},
        ...
    }
}

However, the compiler rejects this code - if I rebind `machine` in a match's arm to the result of a transition like this:

let machine = machine.to_payment();

where .to_payment() has this signature:

fn to_payment(self) -> VendingMachine<Payment>

Then I get the "use of moved value: `machine.state`" error because "`machine` moved due to this [to_payment()] method call, in previous iteration of loop"

How do I implement the behaviour I need?


Solution

  • Rust is a statically typed language, which means that there is no way to change the type of a variable at runtime. It is possible to hide (or shadow) a variable by creating a new variable of the same name:

    let a: i32 = 42;
    let a: String = "foo".to_string();
    

    but these are two different variables and the first one still exists after the second one has been created, as evidenced here:

    let a: i32 = 42;
    let p = &a;
    let a: String = "foo".to_string();
    println!("{}", a);    // Prints "foo"
    println!("{}", p);    // Prints "42"
    

    Playground

    The original a still exists and can still be accessed indirectly through references even after the second one has been created.

    For your use case, if you don't want to use an enum to prevent invalid state transitions, you can use a trait object instead:

    use std::fmt::Debug;
    
    trait State: Debug {
        fn process (self: Box<Self>) -> Box<dyn State>;
        fn is_finished (&self) -> bool { false }
    }
    
    #[derive (Debug)]
    struct Start {}
    impl State for Start {
        fn process (self: Box<Self>) -> Box<dyn State> {
            /* Do something then move to the next state */
            Box::new (InProgress {})
        }
    }
    
    #[derive (Debug)]
    struct InProgress {}
    impl State for InProgress {
        fn process (self: Box<Self>) -> Box<dyn State> {
            /* Do something then move to the next state */
            Box::new (Done {})
        }
    }
    
    #[derive (Debug)]
    struct Done {}
    impl State for Done {
        fn process (self: Box<Self>) -> Box<dyn State> {
            self
        }
        fn is_finished (&self) -> bool { true }
    }
    
    fn main() {
        let mut s: Box::<dyn State> = Box::new (Start {});
        while !s.is_finished() {
            println!("Current state: {:?}", s);
            s = s.process();
            println!("Moved to state: {:?}", s);
        }
        println!("Final state: {:?}", s);
    }
    

    Playground