Rfcs: Matching on an enum shouldn't require full scoping inside the match

Created on 27 Oct 2014  路  18Comments  路  Source: rust-lang/rfcs

Issue by mdinger
_Friday Aug 22, 2014 at 18:16 GMT_

_For earlier discussion, see https://github.com/rust-lang/rust/issues/16681_

_This issue was labelled with: in the Rust repository_


// main.rs
extern crate really;

fn main() {
    let my_enum = really::long::A;

    // This match works with full scoping
    match my_enum {
        really::long::A => {},
        really::long::B => {},
        really::long::C => {},
        really::long::D => {},
        really::long::E => {},
        really::long::F => {},
        really::long::G => {},
    }

    // This doesn't because the scope is missing but the scope
    // doesn't add anything because it's matching variants of an
    // already defined enum.  This should work.
    match my_enum {
        A => {},
        B => {},
        C => {},
        D => {},
        E => {},
        F => {},
        G => {},
    }
}
// lib.rs
pub mod long;
// long.rs
pub enum Enumeration {
    A,
    B,
    C,
    D,
    E,
    F,
    G,
}

I was trying to de-glob libsyntax (https://github.com/rust-lang/rust/issues/11983) but this is a real nuisance there. The enums are much much bigger than this.

I hope this is clear. Let me know if it isn't.

T-lang

Most helpful comment

Don't know what the status is on this, but as a user I'm very much in favor :+1:

All 18 comments

Don't know what the status is on this, but as a user I'm very much in favor :+1:

I鈥檓 somewhat against, because for uncomfortably big matches/enum paths you can use something resembling following:

    { use really::long::*;
    match my_enum {
        A => {},
        B => {},
        C => {},
        D => {},
        E => {},
        F => {},
        G => {},
    }
    }

You don鈥檛 have to import these at the top level, function or block-level import is fine. Also library/module may export unqualified enum variants as well.

Also original motivation does not apply as much anymore either, because glob imports are stable.

I see use really_long::* for this case as a bandage to a problem that shouldn't exist. The use case for glob imports seems like it'd get much smaller if this was fixed too.

You could always write it out long form if you don't like it compact.

This would be nice, but what about derivations? E.g. this would require importing names from two enums, with more potential for name collision:

match (enum1, enum2) {
    (A, X) => {},
    (B, X) => {},
    (B, Y) => {},
    (_, _) => {},
}

Why should we import instead of rewrite

match (enum1, enum2) {
    (A, X) => {},
    (B, X) => {},
    (B, Y) => {},
    (_, _) => {},
}

becomes

match (enum1, enum2) {
    (typeof(enum1)::A, typeof(enum2)::X) => {},
    (typeof(enum1)::B, typeof(enum2)::X) => {},
    (typeof(enum1)::B, typeof(enum2)::Y) => {},
    (_, _) => {},
}

I like the idea. It eases match.
What possible problems it can lead to?

What if X is a constant (or you want it to be a binding variable)?

@sfackler,

enum E {
  A
}

fn main() {
  match E::A {
    0 => {},
    _ => {}
  }
}

returns:

error[E0308]: mismatched types
 --> <anon>:7:5
  |
7 |     0 => {},
  |     ^ expected enum `E`, found integral variable
  |
  = note: expected type `E`
  = note:    found type `{integer}`

So it isn't a problem.

#[derive(PartialEq, Eq)]
enum E {
    A,
}

const X: E = E::A;

fn main() {
    match E::A {
        X => {}
    }
}

Worse:

#[derive(PartialEq, Eq)]
enum E { A, B, C }
const A: E = E::B;  // counter-intuitive but not illegal

fn main() {
    match E::A {
        A => { /* which A? */ }
        _ => {}
    }
}

Backwards compatibility requires that pattern A match the constant (E::B) above, even if this is totally unintuitive, and even if it's ambiguous. Then again, I'm not sure if much _real_ code would get broken if the compiler simply made the ambiguity an error and worked in unambiguous cases.

I think that the opinions from @sfackler, @dhardy are important. I have no counter-argument.
I like how it is made in Swift.

enum E {
case A, B
}

switch E.A {
.A: /* code to handle E.A */
}

But we cannot do

match E::A {
::A => {}
}

because :: means another thing.
I'd like to have .A in Rust, but I don't quite sure that it is a right thing.

Arguably, we should make this existing ambiguity an error, or at least a warning that said which it picked, anyways :

enum E { A, B, C }
const A: E = E::B;  // counter-intuitive but not illegal

{ use E::*;
    match my_enum {
        A => { /* which A? */ },
        _ => {}
    }
}

I think that weakens the case against presented thus far.

A priori, I kinda suspect there is more to be gained from stuff like match and method syntax (.) being generous on name availability, while being aggressive about warning of possible name collisions. I've no actual argument though.

It's a whole different matter if this interacts poorly with say return type inference for fn() -> impl Trait or something. I think not, but maybe.

Also, I could imagine folks wanting trait enums eventually like they are now going for trait fields over getters and setters. I could imagine const being used for a method alias though to, so not sure this feature adds any real complexity.

An alternative might be a weak convention that enum names be kept short in libraries perhaps.

_As always_, the backwards-compatible way to solve ambiguities is to only give meaning to code that _doesn't_ currently compile, i.e. when something doesn't resolve within a pattern.

_In fact_, the any of the options expressed here that are anything like syntactical rewrites _before name resoluton_ are quite inactionable.

Rewriting a Foo(x) pattern to <_>::Foo(x) and hoping for the best inference to do its thing _might_ work.
But that doesn't help _any single example here_, because, well, they all type-check.

match x { a => {} b => {} } will _always_ get past name-resolution and type-checking when a and b don't refer to anything in scope, because they're _variable bindings_.
Only after type-checking _can_ the match be recognized as having _some_ unreachable arms.

F# in comparison is slightly better but not much smarter. It doesn't require full scoping unless if finds an ambiguity. It would look like this in Rust:

enum Two {
    Left,
    Right
}

enum Four {
    Left,
    Right,
    Top,
    Bottom
}

fn main() {
    let four = Four::Top;

    match four {
        Four::Left => {},
        Four::Right => {},
        // ^ Sees these as name collisions when in actuality they should not be
        Top => {},
        Bottom => {},
    }
}

@mdinger Right, and Rust could've done this if it said that a can't be a variant name and A can't be a variable name, but it's probably too late for this now.

Oh, I didn't recognize your previous statement as ranking this as currently implausible (probably due to complete lack of understanding of the compiler internals). It's pretty unfortunate something this convenient would be that compatibility breaking.

If typeof materializes https://github.com/rust-lang/rfcs/issues/1738#issuecomment-244820465, then maybe you could do roughly this with a macro :

macro_rulez! ezmatch {
    ($e:expr { $m:tt }) => {{
        let __ezmatch__ = ;
        use <typeof __ezmatch__>::*;
        match __ezmatch__ { $m }
    }}
}

I'm dubious that'd ever work because the use probably cannot depend upon the typeof, but maybe.

Anyways, if you're worried about conflicting imports inside the match then I think you could write an unambiguous version :

{ use REALLY::LONG as M; match { 
    M::A => {..}, 
    M::B => {..}
} }

Could it be done in epochs?
So in the first epoch, this would be a warning, but in the 2nd one, it'd be an error:

#[derive(PartialEq, Eq)]
enum E { A, B, C }
const A: E = E::B;  // counter-intuitive but not illegal

fn main() {
    match E::A {
        A => { /* which A? */ }
        _ => {}
    }
}

In the 2nd epoch, this would compile:

enum Two {
    Left,
    Right
}

enum Four {
    Left,
    Right,
    Top,
    Bottom
}

fn main() {
    let four = Four::Top;

    match four { // no constants with same name, type is Four, so it knows these are from Four
        Left => {},
        Right => {},
        Top => {},
        Bottom => {},
    }
}

So the purpose of the warning in epoch 1 is to allow people to adjust their code before the change is made in epoch 2.

I don't think we've got anything actionable (or indeed desirable due to the ad-hoc nature of what is proposed) and I am not particularly interested in using the edition mechanism for breakage.
Using Self::Variant and use Self::*; should be ergonomic enough; furthermore, if we get uniform_paths then you don't have to write self::Enum::Variant.

Therefore, I'm closing this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

onelson picture onelson  路  3Comments

3442853561 picture 3442853561  路  4Comments

torkleyy picture torkleyy  路  3Comments

steveklabnik picture steveklabnik  路  4Comments

3442853561 picture 3442853561  路  3Comments