Why is a match pattern with a guard clause not exh

2020-04-09 23:52发布

Consider the following code sample (playground).

#[derive(PartialEq, Clone, Debug)]
enum State {
    Initial,
    One,
    Two,
}

enum Event {
    ButtonOne,
    ButtonTwo,
}

struct StateMachine {
    state: State,
}

impl StateMachine {
    fn new() -> StateMachine {
        StateMachine {
            state: State::Initial,
        }
    }

    fn advance_for_event(&mut self, event: Event) {
        // grab a local copy of the current state
        let start_state = self.state.clone();

        // determine the next state required
        let end_state = match (start_state, event) {
            // starting with initial
            (State::Initial, Event::ButtonOne) => State::One,
            (State::Initial, Event::ButtonTwo) => State::Two,

            // starting with one
            (State::One, Event::ButtonOne) => State::Initial,
            (State::One, Event::ButtonTwo) => State::Two,

            // starting with two
            (State::Two, Event::ButtonOne) => State::One,
            (State::Two, Event::ButtonTwo) => State::Initial,
        };

        self.transition(end_state);
    }

    fn transition(&mut self, end_state: State) {
        // update the state machine
        let start_state = self.state.clone();
        self.state = end_state.clone();

        // handle actions on entry (or exit) of states
        match (start_state, end_state) {
            // transitions out of initial state
            (State::Initial, State::One) => {}
            (State::Initial, State::Two) => {}

            // transitions out of one state
            (State::One, State::Initial) => {}
            (State::One, State::Two) => {}

            // transitions out of two state
            (State::Two, State::Initial) => {}
            (State::Two, State::One) => {}

            // identity states (no transition)
            (ref x, ref y) if x == y => {}

            // ^^^ above branch doesn't match, so this is required
            // _ => {},
        }
    }
}

fn main() {
    let mut sm = StateMachine::new();

    sm.advance_for_event(Event::ButtonOne);
    assert_eq!(sm.state, State::One);

    sm.advance_for_event(Event::ButtonOne);
    assert_eq!(sm.state, State::Initial);

    sm.advance_for_event(Event::ButtonTwo);
    assert_eq!(sm.state, State::Two);

    sm.advance_for_event(Event::ButtonTwo);
    assert_eq!(sm.state, State::Initial);
}

In the StateMachine::transition method, the code as presented does not compile:

error[E0004]: non-exhaustive patterns: `(Initial, Initial)` not covered
  --> src/main.rs:52:15
   |
52 |         match (start_state, end_state) {
   |               ^^^^^^^^^^^^^^^^^^^^^^^^ pattern `(Initial, Initial)` not covered

But that is exactly the pattern I'm trying to match! Along with the (One, One) edge and (Two, Two) edge. Importantly I specifically want this case because I want to leverage the compiler to ensure that every possible state transition is handled (esp. when new states are added later) and I know that identity transitions will always be a no-op.

I can resolve the compiler error by uncommenting the line below that one (_ => {}) but then I lose the advantage of having the compiler check for valid transitions because this will match on any states added in the future.

I could also resolve this by manually typing each identity transition such as:

(State::Initial, State::Initial) => {}

That is tedious, and at that point I'm just fighting the compiler. This could probably be turned into a macro, so I could possibly do something like:

identity_is_no_op!(State);

Or worst case:

identity_is_no_op!(State::Initial, State::One, State::Two);

The macro can automatically write this boilerplate any time a new state is added, but that feels like unnecessary work when the pattern I have written should be covering the exact case that I'm looking for.

  1. Why doesn't this work as written?
  2. What is the cleanest way to do what I am trying to do?

I have decided that a macro of the second form (i.e. identity_is_no_op!(State::Initial, State::One, State::Two);) is actually the preferred solution.

It's easy to imagine a future in which I do want some of the states to do something in the 'no transition' case. Using this macro would still have the desired effect of forcing the state machine to be revisited when new States are added and would just require adding the new state to the macro arglist if nothing needs to be done. A reasonable compromise IMO.

I think the question is still useful because the behavior was surprising to me as a relatively new Rustacean.

1条回答
啃猪蹄的小仙女
2楼-- · 2020-04-10 00:51

Why doesn't this work as written?

Because the Rust compiler cannot take guard expressions into account when determining if a match is exhaustive. As soon as you have a guard, it assumes that guard could fail.

Note that this has nothing to do with the distinction between refutable and irrefutable patterns. if guards are not part of the pattern, they're part of the match syntax.

What is the cleanest way to do what I am trying to do?

List every possible combination. A macro could slightly shorten writing the patterns, but you can't use macros to replace entire match arms.

查看更多
登录 后发表回答