Bring enum variants in scope for patterns

Am I missing something? Why not LanguageType::Rust.line_comments()?

4 Likes

It holds some relevant information in a few cases.

enum FoosOption<T> {
    Some(T),
    None,
}

match ... {
    // Wait, is it `::core::option::Option::Some` from prelude?
    Some(_) => {},
    // The pattern might also be the source of type inference?
    None => {},
}

I'd really like those choices to be visible. However, I do think I see where you are coming from and would prefer a solution that is not as heavy on symbols either. I also don't think that _ is a particular good choice in that regard either. Especially within patterns its use is for an item that is ignored, not one that is inferred. Just slightly more verbose but, imho, much more readable would be to use the keyword enum itself as infer this enum type for me.

match FoosOption::None {
    enum::Some(_) => {},
    _ => {},
}
3 Likes

No you're not missing something. I guess I thought that was unintuitive enough that it couldn't work. Glad to know that it does, though personally I would stick to either of the previous two forms over this one.

I'm in way over my head here (first post, only been using rust a few months), but when I was first getting started with the language I expected to be able [edit, learned it's possible, so] for it to be more common to:

enum X {
    Y(bool),
    Z
}
use X::*;
let x: X = Y(true);

which seems like it would solve the match usability issue without any ambiguity (in that the rules for shadowing would be the same as with other use invocations), allow for shorter local renaming, and be consistent with (all?) other occurrences of :: in the language. What are the downsides of this approach?

You can already do that, what we are talking about here is destructuring enums without having to explicitly bring the enum variants into scope. Like this

enum X {
    Y(bool),
    Z
}

match x {
    Y(_) => todo!(),
    Z => todo!(),
}
// or
match x {
    _::Y(_) => todo!(),
    _::Z => todo!(),
}
2 Likes

I don't understand @jbr as saying you cannot do that. But I do find it compelling that they are new to the language and have expectations that align well with how the language works. That suggests to me that things work fine as they are -- and that learnability won't be improved by adding more special cases. Indeed, I largely do agree that use X::*; is good when necessary, and agree with @petrochenkov that Foo::Variant reads better. There's also Self::Variant which is often usable.

2 Likes

Thanks, I didn't realize that worked! I apparently don't have the usability problem presented by OP

1 Like

It isn't very informative when reading code. But it helps when writing code, especially for newer users, because it makes the mental model of how names come into scope simpler and more consistent. Having an explicit marker also means we can support the syntax in all contexts, like Swift does, not just in match patterns. In particular, I think it would be a big win in function arguments.

--

On the other hand, I strongly dislike the specific proposed syntax _::Foo. _:: is annoying to write (hard to type without significant hand movement on a QWERTY keyboard), and annoying to read (too long). If you need something that heavyweight, you might as well just write out the enum name instead, which is even heavier but at least compensates by being informative. I'd prefer a lighter-weight syntax, like :Foo.

10 Likes

Does anyone have a sense of how often matching against consts is used? While we now have good lints for the problem, the confusion between bindings and consts is well-documented. (It's even in SML criticisms from 1998, showing how must Rust is actually ML in C clothing :wink: )

I'd be really tempted to require const Bar => or const { 2 * N } => or similar for non-literal constants. Or {}, like in const generics. Or something.

7 Likes

In rustc, it's used somewhat frequently to pattern match on Symbols, using the always qualified form sym::my_symbol (use rg "sym::" src/lib*). The use there makes the code more readable in my view and I don't think const Bar would be helpful.

1 Like

It's also used relatively commonly with extensible data structures, using associated consts as a way to represent known variants of non-exhaustive enums defined by external documentation (example above).

I think from teachability perspective not requiring any prefix is the simplest. You match on an enum, you use a name of the variant, and it just works. When there's nothing needed but the enum variant name, any novice user can just type the first thing they expect to work (whether it's namespaced or non-namespaced variant name), and it will work.

Wheras with _::, you first have to know it exists. It's possible to guess it's a thing from knowing _ and ::, but IMHO it's a clever mashup of two features, and not something immediately obvious. Especially that other languages don't have same syntax, and AFAIK only Swift has any syntax for this at all. When reading code, an unfamiliar user could still guess what it means, but it requires stopping and mentally processing that syntax.

_:: would still be useful for function arguments, so I wouldn't mind if it was in the language, but for patterns it's less ideal than just using variant names.

4 Likes

Probably not for Java folks. Java variants are in scope for switch statement only. It felt weird at first but learn it we did. Now feels very convenient - less boilerplate. Brains adjust. People will learn and likely appreciate; even if variants are not in scope for anything else.

2 Likes

The problem is when you then try to use the name of the variant somewhere else, and it doesn’t work. :slight_smile: At least, that’s the biggest concern I have from a teachability perspective.

4 Likes

I get this is a problem, but the compiler already knows how to fix this error, so it's not a dead-end for users, but a matter of following compiler's suggestion.

let foo = Bar;

help: possible candidate is found in another module, you can import it into scope
   |
1  | use crate::Foo::Bar;
   |

This particular error suggestion could be improved to give enum-specific fix. So assuming the compiler can help with the right syntax, the remaining difference is what the nicest syntax can look like.

2 Likes

Ah, thanks. Probably too much churn to consider, then.

1 Like

It still requires the user's mental model to be more complex if they want to actually understand when they need the use and when they don't, as opposed to guessing and hoping the compiler will give a good suggestion if they guess wrong.

...Perhaps not much more complex. I may be biased against the idea because of its resemblance to C++'s horrid argument-dependent lookup and other namespacing rules, even though the rule being proposed here is far, far less complex.

Still, it's important to keep in mind that even perfect diagnostics don't eliminate the cost of complexity, only reduce it. And while the diagnostic you mentioned is quite good, it's not perfect, at least not in the sense that the user can mechanically accept the suggestion whenever they see it. After all, there isn't always just one candidate, and sometimes none of the candidates are correct (because your actual mistake was something other than a missing import – e.g. the name might be intended to refer to something defined in the current module, but you forgot to define it).

I think this is the point where we need to compile a weighted list of pros and cons, and make a principled decision from there.

Otherwise this will just go back and forth for weeks until everybody forgets about this. It really doesn't seem like further debate is going to bring more information, just differences of taste.

11 Likes

With string literals inferring enum, it'd work pretty well, no compatibility break.

match x {
    "bar" => ...
    "baz" => ...
    "quux" => ...
    "another-variant" => ...
}

This'd be appropriate for tagged enums. Every variant'd consist of (str, num) at compile time. (str is auto selected while defining variants.)

Using string literals for enums has been a convention in other languages (ActionScript 3.0 for instance), but in Rust it can just be static (no dynamic string involved).

What if this were applied to non tagged enums? Let's see:

match x {
    "ok"(x) => ...
    "err"("expecting-token") => ...
}

This could be supported, not sure.

This is way too different from existing Rust semantics.

6 Likes