Bring enum variants in scope for patterns

For me the above examples provide the deciding weight on which approach to favor.

With this example, I find that if we allow eliding enum name in a pattern but not in an expression it would feel very asymmetric. That is, the following should be possible:

fn do_something() -> Result<(), LibraryError> {
    ...
    return Err(FailedRequest(ConnectionFailed(Timeout)));
    ...
}
1 Like

It would be nice, but it's a separate feature which could be added later. It may also be harder and more controversial.

I see bringing variants into scope of matches as an extension of match ergonomics. We already have some "magic" that is only for match, but not for expressions. In match you can ignore the difference between Some(foo) and Some(ref foo), but when generating such value Rust won't let you ignore the difference between Some(foo) and Some(&foo).

How would this work with type inference?

Currently this compiles:

fn main() {
    match 1u32.into() {
        Some(_) => {},
        None => {}
    }
}

which means the arms influence inference back to the type being matched. So if the type of the match isn't established, then scope of the arms may be unclear? Won't that be a chicken-egg problem?

5 Likes

I personally like languages that lack magic. Either have scoping rules or don't. But please don't be wishy-washy about it. Some of this just sounds like a request for Rust's enums and scoping rules to be like C++'s (non-class) enum scoping rules or ADL, which I'm very glad Rust did not adopt.

If Rust adopted some kind of "infer the scope please" I would prefer _::Name syntax to make it clear to users you're referring to something that is currently not in scope and are requesting Rust to automatically infer that. But anything less explicit than that goes against Rust's philosophy on explicitness.

20 Likes

I think that _:: syntax would solve both problems:

_::FailedRequest(_::ConnectionFailed(_::Timeout))) => true,
    return Err(_::FailedRequest(_::ConnectionFailed(_::Timeout)));

That also avoids the "where did that name come from" problem when reading the code, and makes it explicit that some manner of inference is going on.

17 Likes

I'd like to voice a strong advocate for adding the _:: syntax. I've been writing quite a bit of Swift lately for a project, and enum inference is easily the number one thing I want Rust to steal from Swift. Putting enums in your public API in Rust makes it feel quite clunky as you often have to import the enum in any module that calls those functions. Where as in Swift using an enum in your API feels as intuitive as using any other data type, especially when you have some kind IDE integration like in XCode.

I would be willing to dedicate some time towards writing an RFC and implementation for this, though right now I don't know how much time and I definitely wouldn't feel comfortable working alone on it.

13 Likes

I'd be happy to provide review and feedback for any such RFC.

1 Like

I'm a bit reserved about adding _::, you can as it is you can replace _ with a variable x to bind the variable, however this usage of _ would not bind to a value as it isn't a value. Whenever i've run across cases like this and I don't want to use Whatever::*; I'll just use Whatever as W; to bind it to a short name within the scope.

I'd probably continue doing so even if '_::' were allowed since I a list of the things in scope at the top of the function, and it seems clearer to readers not familiar with the code than _.

1 Like

Underscore (_) is used in more positions than variable bindings (e.g. '_ lifetimes, or type inference like .collect::<Vec<_>>()), so I don't really follow that argument.

Whenever i've run across cases like this and I don't want to use Whatever::*; I'll just use Whatever as W; to bind it to a short name within the scope.

I'd probably continue doing so even if '_::' were allowed since I a list of the things in scope at the top of the function, and it seems clearer to readers not familiar with the code than _ .

The issue I brought up was having to import anything at all, so I don't find use as to be a solution. At best it's a workaround. The inclusion of enum inference doesn't take away from you being able to be explicit, just like how type inference doesn't make it any harder to be explicit about your types if that's what you desire.

1 Like

True, but they aren't overriding it in 2 different ways within the same expression.

Without any syntactic marker this won't fly with the compiler structure as it exists now.

Things with any syntactic marker like match Foo::Bar { .Bar => {} } or match Foo::Bar { _::Bar => {} } can work.

_::Bar is an especially bad syntactic marker though because it tries to look like a type-relative path while being something entirely different, and without the path analogy it's just an ugly prefix operator _::.

(I don't think the syntactic marker buys us much compared to use Foo::*;, I personally don't even use globs and always write Foo::Bar because it has better searchability and maintainability.)

11 Likes

So, in summary, either 1) prepend the type prefix or 2) define a type alias and prepend that.

Can't comment on the implementation side, but can comment on the user experience: I assert that including a marker like _:: encodes no information relevant to users.

The pattern must be a valid destructuring of the type being destructured: for non-nullary enums, this means it must be a variant; for nullary enums, it could be a variant, a binding, or a const. Bindings and consts are not in PascalCase in idiomatic Rust whereas variants are: it will be clear whether this is a variant, a binding or a const based on the casing. _:: therefore is a redundant encoding of the fact that this pattern is a variant, and encodes no information at all for nonnullary variants.

18 Likes

After some thought, perhaps it can be done without rearchitecting everything if we make all ambiguous identifiers in patterns produce name definitions before type checking, with those definitions being able to turn into "symlinks" after type checking.

enum Foo {
    Bar // 1
}

fn main() {
    match Foo::Bar {
        // Before type checking: defines name Bar (2) as "Def::Ambiguous"
        // After type checking: refers to Bar (1)
        Bar => {
            // Before type checking: refers to Bar (2)
            // After type checking: ultimately refers to Bar (1) through "symlink" Bar (2) 
            let x = Bar;
        }
        // Before type checking: defines name baz (3) as "Def::Ambiguous"
        // After type checking: do not refers to anything, identified as a fresh binding
        baz => {
            // Before type checking: refers to baz (3)
            // After type checking: still refers to baz (3) because it's not a "symlink" 
            let x = baz;
        }
    }
}

Someone needs to try and prototype this, I'm still not sure that we are not relying on identifying local variables early (e.g. for "match ergonomics" or something).

5 Likes

That's definitely true for the pattern matching, though I would say I am more interested in the general case of using enums rather than pattern matching specifically, which would require some kind of prefix. To show a specific example, you could write a case conversion API like the following.

pub enum CasingStyle {
  CamelCase,
  SnakeCase,
  KebabCase,
}

impl String {
  pub fn convert_case(&self, style: CasingStyle) -> Self {
    // impl body
  }
}

// Somewhere else in another file.
let string = String::from("HelloWorld");
assert_eq!(string.convert_case(_::SnakeCase), "hello_world");
6 Likes
enum E {A, B}; ...
switch (e) { case A: ...; case B:...; }

is in the blood of Java people

1 Like

Another more real world example that came to mind is improving the ergonomics using methods on enums themselves. Right now tokei has the LanguageType enum as one of the main pieces of its API. If you're using tokei as a library currently using the enum feels a bit clunky. Currently you can only call enum methods in either of these two ways.

LanguageType::line_comments(LanguageType::Rust);
// or
let rust = LanguageType::Rust;
rust.line_comments();

where as with a enum inference prefix you could instead write it like the following;

LanguageType::line_comments(_::Rust);
1 Like

I think your approach makes sense if we were to attempt this. We'd need to make some tweaks resolve_pattern_inner and propagate to the type checker. For details on pattern type checking, in particular default binding modes, see:

It encodes the information "This name was not in scope, but a kind of inference is letting me use it anyway". Specifically, it reuses the known construct of :: and the known inference-marker of _ to specify inference of the type containing the named variant, both of which convey meaning using constructs existing users may already have some understanding of.

Also, as a meta-comment, I would like to suggest that "encodes no information relevant to users" is an assertion that is straightforwardly refuted: I'm a user, and it encodes information relevant to me, N=1. We could perhaps discuss whether that information could be conveyed another way, or what proportion of users we believe will value that information based on our likely different experiences with users, but a general assertion it "encodes no information relevant to users" feels like an absolute that's either easily refuted or not conducive to finding a consensus.

This is the kind of inference that makes rustc more painful for me to use, and Rust code more difficult for me to read. "the type being destructured" isn't a thing immediately visible by reading the code.

17 Likes