How do I relax the non-exhaustive patterns check for a nested match on known variants?
Asked Answered
K

2

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

Kroeger answered 8/5, 2019 at 21:17 Comment(0)
E
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.

Eatable answered 9/5, 2019 at 0:37 Comment(3)
> Conceptually, nothing prevents you from doing o = Op::Add between the outer match and the inner match. o is immutable.Hokku
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?Kinnard
It is valid to rebind the variable (i.e. do a let o = ... inbetween) playground: play.rust-lang.org/…Darrondarrow
M
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.

Mescaline answered 8/5, 2019 at 21:35 Comment(3)
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.Kroeger
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.Kroeger
@Kroeger 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.Mescaline

© 2022 - 2024 — McMap. All rights reserved.