Bring enum variants in scope for patterns

I think it would be prudent to take the actual implementation complexities of different proposals into account. I feel much of this has been argued in the abstract wrt. "taste" without thinking so much about how to make the compiler behave as we'd like, how complex it would be, or how it would affect the specification cost.

1 Like
macro_rules! with {
    ($($q:path),*: $x:expr) => {{$(use $q::*;)* $x}}
}

enum Color {Green, DarkGreen}

fn main() {
    let color = Color::DarkGreen;
    let text = with!(Color: match color {
        Green => "Green",
        DarkGreen => "DarkGreen"
    });
    println!("{}",text);
}

Disclaimer: I haven't read the whole thread, 64 posts in 3 days are quite many so please forgive me if this was already brought up.

I have a strong opinion against bringing enum variants into scope for the same reason as clippy::enum_glob_use. It's very easy to introduce bugs as a wrongly typed enum variant will be interpreted as a variable which will match everything. While in most cases this will at least end up in a warning, it's also possible to not generate a warning. Even when generating a warning the chance is high that this will occure when other warnings are around (of which Rust has plenty, which is good!) and this warning won't be noticed as you are searching for a bug which is based on a wrongly typed enum variant which you expect to be a compile error instead.

7 Likes

How about using :: rather than _::, _ already have other meanings inside match which usually we might think a wildcard in Rust (e.g. '_, Vec<_>) but _ in _:: is not a wildcard but a different meaning instead? Example,

match x {
    ::A(_) => {},
    ::B(_) => {},
    ::C(::A(_)) => {},
}
1 Like

Hmm. ::A already has a meaning (global path), but I suppose it's become mostly redundant as of Rust 2018...

1 Like

Isn't :: still needed even with uniform paths to disambiguate in the case of a collision between a local name and a dependency's name?

I'd also really like to have this. After reading through all the existing comments here, I'm now of the opinion that the proposed _::Variant is the best solution, and just bringing the variants into scope directly is not feasible, for the reasons mentioned already and one that hasn't been mentioned: It makes adding variants to #[non_exhaustive] enums a potential breaking change:

#[non_exhaustive]
enum MyEnum { A, B }

fn match_on(e: MyEnum) {
    match e {
        A => println!("a"),
        B => println!("b"),
        // if other variants are added without C being added, they take this branch
        C => println!("c"),
        // if C and other variants are added, those other variants take this branch
        _ => println!("_"),
    }
}

Additionally, when accidentally looking at the docs for a newer version of a dependency that defines more variants for an enum than are actually visible in ones program, it's easy to accidentally introduce catch-all branches (same as when the variants are typo'd as @TimDiekmann mentioned).

10 Likes

Yeah, automatically bringing into scope comes with all the problems of use MyEnum::*

Just for completeness, alternative to globs which I use to shorten matches use MyEnum as ME;, and ME::C doesn't suffer from this issue and works already.

Having dealt with plenty of cases of enums with overlapping members and conversion between them, I'd much rather see Foo::A => Bar::A, than _::A => _::A, as it gives a hint about the type of the parameter, and the type of the return.

Or is _:: and equivalent to MyEnum as _; within the scope of the match? in which case that has to be _::A => Bar::A?

1 Like

There's two parties here: "type based enum lookup in patterns," and "type based enum lookup everywhere."

The former would allow match foo { _::A => () } but not takes_foo(_::A). The latter party would allow the latter pattern to work, for some inference threshold.

The question of the "type based variant lookup" is one of when the type is "obvious" enough to do type based lookup. For the former party, the answer is that the type is (should be?) clear in patterns. For the latter party, the line is harder to nail down.

To use the Swift example (imaginary API), it is nice to be able to do

button.set(state: .Pressed)

but would it also make sense to do

let state = .Pressed
button.set(state: .Pressed)

(I legitimately don't know whether this works, I strongly suspect it doesn't.)

What about

var state = State.Released
state = .Pressed

(I suspect this one probably works, modulo me not having used Swift in years, so the syntax may be off.)

Rust probably has an answer in the form of the existing HM (IIRC) type inference algorithm, but it's an interesting trade-off between inference and explicitness (but remember: the quality to me measured and optimized for is local clarity, not explicitness).

I think this feature, due to it's necessary "punctuation soup," doesn't really pull weight unless it works outside of pattern position. It would be a much bigger benefit (imho) to nice API design (in reducing the cost of taking an enum rather than, say, a bool) than to match expressions.

I think the syntax unfortunately has to be _:: if it's anything. ::ident is unambiguously a crate name, and that's one of the primary benefits of the path rework that edition2018 brought. .ident could work but would be objectively wrong, as Rust uses :: to go from enum name to variant name. :ident might work, but also seems quite foreign (and easily confused with type ascription).

3 Likes

This wouldn't work in Swift, but not because .Pressed wouldn't be "in scope", but because Swift does not permit type inference across statements, and the let state statement doesn't have sufficient type constraints to determine a type to Swift's satisfaction.

This does work in Swift.

Although note that Swift style is to use lowerCamelCase for enum cases.


As an additional thing from Swift I think may be of relevance to this discussion: In Swift, patterns must use let if they are introducing a new identifier. in Rust-ish syntax:

#[non_exhaustive]
enum MyEnum { A, B }

fn match_on(e: MyEnum) {
    use MyEnum::*;
    match e {
        A => println!("a"),
        B => println!("b"),
        // Invalid:
        // C => println!("c: {}", C),
        // Valid:
        let C => println!("c: {}", C),
    }
}

This would, of course, be a big breaking change, but it would mean that then adding a new case (with the OP proposal or use MyEnum::*) or adding a new const (if the enum is #[derive(PartialEq, Eq)]) is less breaking.

5 Likes

It isn't very informative when reading code. But it helps when writing code ...

I expect Rust to value maintainability, which includes prioritising code readers over code writers.


Will matches on enum join the list of things that require global project search or compiler/IDE help to find declaration of:

  • Misuse of use ...::*;
  • Inherent impl methods
  • Macro-generated items
1 Like

Just as a minor point, this depends on which QWERTY layout you have. For example, the Finnish layout is mostly very bad for programming, but _:: happens to be quite easily typed. Finnish, of course, is a small language, but for instance the German layout is similar in this respect.

2 Likes

Excellent point. On a German keyboard typing the full _:: takes about as long as creating a single opening curly brace {. Actually, from an ergonomic standpoint on the (German) QUERTZ keyboard I would propose replacing all { with _:: because the latter requires far less hand movement /s.

Edit: Just looked up American QUERTY layout. Apparently, your guys’ hand movement requirements for typing _:: is still far less than what a single { on a German or Finnish keyboard takes. Also this statement:

as far as I can tell is nothing but a strong exaggeration. An _ on American QUERTY is only slightly less convenient than a capital P. The :: would be the same with the type name or the underscore.

3 Likes

For once, (German) QWERTZ keyboards have an advantage. :smiley: (_ and : are directly next to each other and both need shift, so _:: is typed very easily.)

Keyboard layouts were designed for typing text, so all sigils except those most used in natural language (,, .) will be somewhat inconvenient.

Given that, I don't understand why people with American-English QWERTY keyboards claim that _:: is hard to type; the _ is two rows above the :, both typed with the right little finger while holding a shift with the left little finger.

It is a more difficult sequence on the American Dvorak keyboard: _ with the right little finger while holding shift with the left little finger, followed twice by : with the left little finger while holding shift with the right little finger. I don't consider that a deal-breaker; people who use the Dvorak keyboard layout are used to weird reaches when typing computer shortcuts, such as CTRL+x where x ∈ {Z, X, C, V}.

Opened https://github.com/rust-analyzer/rust-analyzer/issues/4014 for an IDE-side mitigation.

EDIT: fixed in the latest release

8 Likes

Could we add .use operator to import all enums mentioned in the type of a target expression into scope?

match x.use {
    Foo => Bar,
    Baz(Qux) => Quux,
}
// vs
match x {
    Foo::Foo => Qux::Bar,
    Foo::Baz(Qux::Qux) => Qux::Quux,
}
1 Like

As a user of the language, if Self::Variant worked in enum impl blocks, this would already improve 80% of the cases where I want match patterns to be shorter.

1 Like

It works since Rust 1.37.

You can now use enum variants through type alias.

(Self is a type alias)

Link to godbolt for the code below

pub enum Foo{
    Bar,
    Baz(u32),
    Qux{
        x:u32,
        y:u32,
    }
}

impl Foo{
    pub fn fooize(self)->u32{
        match self {
            Self::Bar=>0,
            Self::Baz(f0)=>f0,
            Self::Qux{x,y}=>x+y,
        }
    }
}
9 Likes

Oh, looks like I haven't been as up-to-date as I thought I was. Thank your for the pointers!

4 Likes