5

How do I persuade the Rust compiler that the internal match expression is fine here, as the outer match has already restricted the possible types?

enum Op {
    LoadX,
    LoadY,
    Add,
}

fn test(o: Op) {
    match o {
        Op::LoadX | Op::LoadY => {
            // do something common with them for code reuse:
            print!("Loading ");

            // do something specific to each case:
            match o {
                // now I know that `o` can only be LoadX | LoadY,
                // but how to persuade the compiler?
                Op::LoadX => print!("x"), /* LoadX specific */
                Op::LoadY => print!("y"), /* LoadY specific */
                _ => panic!("shouldn't happen!"),
            }

            println!("...");
        }

        Op::Add => println!("Adding"),
    }
}

fn main() {
    test(Op::LoadX);
    test(Op::LoadY);
    test(Op::Add);
}

I tried two approaches, but neither seems to work.

  1. Name the or-pattern and then match using that name:

    match o {
        load@(Op::LoadX | Op::LoadY) => {
        // ...
        match load {
            // ...
        }
    } 
    

    That's not valid Rust syntax.

  2. Name and bind every constructor:

    match o {
        load@Op::LoadX | load@Op::LoadY => {
        // ...
        match load {
           //...
        }
    } 
    

    That still doesn't satisfy the exhaustiveness check, hence the same error message:

    error[E0004]: non-exhaustive patterns: `Add` not covered
      --> src/main.rs:14:19
       |
    14 |             match load {
       |                   ^ pattern `Add` not covered
    
    

Is there any idiomatic way of solving this problem or should I just put panic!("shouldn't happen") all over the place or restructure the code?

Rust playground link

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
dying_sphynx
  • 1,136
  • 8
  • 17

2 Answers2

4

You cannot. Conceptually, nothing prevents you from doing o = Op::Add between the outer match and the inner match. It's totally possible for the variant to change between the two matches.

I'd probably follow Stargateur's code, but if you didn't want to restructure your enum, remember that there are multiple techniques of abstraction in Rust. For example, functions are pretty good for reusing code, and closures (or traits) are good for customization of logic.

enum Op {
    LoadX,
    LoadY,
    Add,
}

fn load<R>(f: impl FnOnce() -> R) {
    print!("Loading ");
    f();
    println!("...");
}

fn test(o: Op) {
    match o {
        Op::LoadX => load(|| print!("x")),
        Op::LoadY => load(|| print!("y")),
        Op::Add => println!("Adding"),
    }
}

fn main() {
    test(Op::LoadX);
    test(Op::LoadY);
    test(Op::Add);
}

should I just put panic!("shouldn't happen")

You should use unreachable! instead of panic! as it's more semantically correct to the programmer.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 4
    > Conceptually, nothing prevents you from doing o = Op::Add between the outer match and the inner match. `o` is immutable. – Mike Mueller Dec 03 '19 at 09:07
  • 2
    as @MikeMueller stated, i also wonder if in the case of immutable variables conceptionally it should be possible, right? or is there another reason why this cannot work? – farukg Mar 18 '20 at 15:10
3

I think that you just need to refactor your code, obviously LoadX and LoadY are very close. So I think you should create a second enumeration that regroup them:

enum Op {
    Load(State),
    Add,
}

enum State {
    X,
    Y,
}

fn test(o: Op) {
    match o {
        Op::Load(state) => {
            // do something common with them for code reuse
            print!("Loading ");

            // do something specific to each case:
            match state {
                State::X => print!("x"),
                State::Y => print!("y"),
            }

            println!("...");
        }

        Op::Add => println!("Adding"),
    }
}

fn main() {
    test(Op::Load(State::X));
    test(Op::Load(State::Y));
    test(Op::Add);
}

This make more sense to me. I think this is a better way to express what you want.

Stargateur
  • 24,473
  • 8
  • 65
  • 91
  • 1
    Yes, restructuring the code will help of course. But I was more interested in hinting the compiler in the original case. However, probably most of the real world cases can be handled in a way you propose. – dying_sphynx May 08 '19 at 21:49
  • In my situation I have lots of 3-letters assembly mnemonics for opcodes like: LDX, LDY, LDA, STX, STA, etc. It would be nice to keep them as is: just flat enum values, so that they can match original mnemonics. Otherwise, it would be something like LD(X), which is not as readable. – dying_sphynx May 08 '19 at 21:54
  • 2
    @dying_sphynx so you want a flat enum but still be able to regroup them, you can't have both I think, it's opposite. Maybe there is a crate that flat nested enum. But still I don't advice it, I found `Load(Register::X)` way more readable that LDX. Remember, you are doing Rust, not assembly. Express your virtual machine in Rust, don't just copy assembly. Anyway, my solution or `unreachable!` solution should produce similar benchmark result so it's really up to you. – Stargateur May 08 '19 at 22:01