2

I hit an interesting spurious "match in match" non exhaustive error with rust. To make a small simple example (of course the production case is more interesting):

pub enum SomeEnum {
    Possibility1,
    Possibility2,
    Possibility3
}

pub fn do_something(some: &SomeEnum){
    match some {
        SomeEnum::Possibility1 => println!("this is possibility 1"),
        something_else => {
            println!("this is not possibility 1");

            match something_else {
                SomeEnum::Possibility2 => println!("but this is 2"),
                SomeEnum::Possibility3 => println!("but this is 3")
            }
        }
    }
}

does not compile, with an error:

error[E0004]: non-exhaustive patterns: `&Possibility1` not covered
  --> src/main.rs:13:19
   |
1  | / pub enum SomeEnum {
2  | |     Possibility1,
   | |     ------------ not covered
3  | |     Possibility2,
4  | |     Possibility3
5  | | }
   | |_- `SomeEnum` defined here
...
13 |               match something_else {
   |                     ^^^^^^^^^^^^^^ pattern `&Possibility1` not covered
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
   = note: the matched value is of type `&SomeEnum`

I understand this error, and I know how to "brutally" fix it (similarly to Non-exhaustive patterns - Rust match expressions by adding a:

_ => unreachable!();

branch for example).

Still, I feel this is very sad: the compiler is smart enough at the first match to know that the something_else is either of kind Possibility2 or Possibility3, while toss away this knowledge down the code?

My question is therefore: is there a simple, idiomatic way to force rust to "propagate" this kind knowledge from the outer match to the inner match so that I can suppress the error without brutally matching all inputs? Again, in this simple example the _ match may seem like the no-brainer, but in some more complex code, I would like to have help from the compiler checking that my logics are right, and this disqualifies using a _ match-it-all filter.

Zorglub29
  • 6,979
  • 6
  • 20
  • 37

1 Answers1

4

Still, I feel this is very sad: the compiler is smart enough at the first match to know that the something_else is either of kind Possibility2 or Possibility3

It's not, really. Semantically it just tries every match arm one after the other and takes the first.

My question is therefore: is there a simple, idiomatic way to force rust to "propagate" this kind knowledge from the outer match to the inner match

No. rustc reasons at the type level (mostly), as far as it's concerned something_else is a SomeEnum, and SomeEnum has a SomeEnum::Possibility1. If you want a sub-enum, you have to provide one.

This sort of things might eventually possibly be workable if RFC 2593 Enum Variant Types (or an alternative like OCaml-style polymorphic variants) gets tabled again and accepted, and implemented. But short of that you'll have to implement it "by hand" on your enum.

By "do it by hand" I mean manually encode the semantics you want as additional enums e.g.

pub enum SomeEnum {
    Possibility1,
    Possibility2,
    Possibility3
}

enum IsOne { One, Other(Sub23) }
enum Sub23 {
    Sub2,
    Sub3
}
impl SomeEnum {
    fn is_one(&self) -> IsOne {
        match self {
            Self::Possibility1 => IsOne::One,
            Self::Possibility2 => IsOne::Other(Sub23::Sub2),
            Self::Possibility3 => IsOne::Other(Sub23::Sub3),
        }
    }
}

pub fn do_something(some: &SomeEnum){
    match some.is_one() {
        IsOne::One => println!("this is possibility 1"),
        IsOne::Other(sub) => {
            println!("this is not possibility 1");

            match sub {
                Sub23::Sub2 => println!("but this is 2"),
                Sub23::Sub3 => println!("but this is 3")
            }
        }
    }
}
Masklinn
  • 34,759
  • 3
  • 38
  • 57
  • Thanks for the detailed answer :) . Ok, thank you for the link to the RFC. I agree that this may probably be solved by the RFC you mention, but do I understand correctly that this RFC ask for changes that are much more general in scope than what would be needed to make this work? :) In a sense. the only thing needed to solve my example would be to "forward" the knowledge of which cases are already matched and cannot be a part of ```something_else``` together with it, right? :) . – Zorglub29 Sep 21 '21 at 13:14
  • 1
    " do I understand correctly that this RFC ask for changes that are much more general in scope than what would be needed to make this work? :)" in the sense that what you want is one application of the RFC, sure. – Masklinn Sep 21 '21 at 13:15
  • Yes, thanks for the example with "sub-enums", that sounds a bit verbose, but agree this could be a solution :) . – Zorglub29 Sep 21 '21 at 13:15
  • 1
    "In a sense. the only thing needed to solve my example would be to "forward" the knowledge of which cases are already matched and cannot be a part of something_else together with it, right? :) ." this is not a thing that exists, so it's not like it's going to magically appear out of thin air. Variants as types would be the most likely proposal to cover this, the alternative would be some sort of subrange type except enumerated (so not really a *range* per-se), and Rust currently has no support at all for subrange types. – Masklinn Sep 21 '21 at 13:17
  • In fact while you can have subrange-ish types with const generics ("bounded" types), that would not work at all for your use case. – Masklinn Sep 21 '21 at 13:18
  • Ok, I am not familiar with how the matching mechanics are implemented, thanks for the explanations :) . – Zorglub29 Sep 21 '21 at 13:22
  • 1
    Rather than enum variant types, to support this feature I think the language would really need refinement types (similar to discerning, when matching on an integer and the first arm’s pattern encompassing some range of values, that the following branches necessarily exclude that range). – eggyal Sep 21 '21 at 13:38
  • @eggyal both would work but I don't think I've ever seen a proposal for full-blown refinement types for rust, only for subranges at best (RFC 671). – Masklinn Sep 21 '21 at 13:52
  • I'm not sure I completely agree that type system extensions are necessary to make this work - the compiler already does reachability analysis in some (very limited) cases, and it is possible to extend this to suppress the exhaustiveness check for `match`, at least in easy cases like this one, without changing how the type system analyzes the code (just do the equivalent of inserting a `_ => unsafe { unreachable_unchecked!() }` arm, only when provably correct, of course). Whether this would be desirable and worth the added complexity is certainly up for debate. – trent Sep 21 '21 at 14:39
  • @trentcl it's not necessary to make the code work but it's necessary to make the code work safely OOTB. Kinda like lifetime analysis is not required to make borrowing correct. – Masklinn Sep 21 '21 at 15:45