Skip to content

Allow inferring enum variant types with _::Variant syntax #3167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
fourbytes opened this issue Aug 23, 2021 · 28 comments
Open

Allow inferring enum variant types with _::Variant syntax #3167

fourbytes opened this issue Aug 23, 2021 · 28 comments

Comments

@fourbytes
Copy link

I’ve recently been bouncing between Swift and Rust and have found the shorthand .variant syntax to be very useful. Having something similar in Rust would be very nice to have.
As initially suggested in #2830, the proposal is to allow explicitly inferring types for enum variants by using a _:: prefix.

Examples

enum CompassPoint {
    North,
    South,
    East,
    West
}

let mut direction = CompassPoint::West;
direction = _::East;
match point {
  _::East => { ... }
  _::West => { ... }
  _ => { ... }
}
fn function(cp: CompassPoint) {}

let direction = _::West;
function(direction);
@Fishrock123
Copy link
Contributor

You should already be able to import enum variants directly:

use CompassPoint::West

@shepmaster
Copy link
Member

shepmaster commented Aug 30, 2021

You should already be able to import enum variants directly:

That's true, but importing the variants can cause other name clashes:

struct Fahrenheit(f32);
struct Celcius(f32);

enum Temperature {
    Fahrenheit(Fahrenheit),
    Celcius(Celcius),
}

impl Temperature {
    fn flip(self) -> Self {
        match self {
            Self::Fahrenheit(f) => Self::Celcius(Celcius(todo!("math"))),
            Self::Celcius(f) => Self::Fahrenheit(Fahrenheit(todo!("math"))),
        }
    }
}

If we import the variants:

impl Temperature {
    fn flip(self) -> Self {
        use Temperature::*;
    
        match self {
            Fahrenheit(f) => Celcius(Celcius(todo!("math"))),
            Celcius(f) => Fahrenheit(Fahrenheit(todo!("math"))),
        }
    }
}
error[E0308]: mismatched types
  --> src/lib.rs:14:38
   |
14 |             Fahrenheit(f) => Celcius(Celcius(todo!("math"))),
   |                                      ^^^^^^^^^^^^^^^^^^^^^^ expected struct `Celcius`, found enum `Temperature`

error[E0308]: mismatched types
  --> src/lib.rs:15:38
   |
15 |             Celcius(f) => Fahrenheit(Fahrenheit(todo!("math"))),
   |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Fahrenheit`, found enum `Temperature`

Something like _:: would work because it doesn't cause conflicts.

As a workaround, I sometimes rename the enum to something short:

impl Temperature {
    fn flip(self) -> Self {
        use Temperature as E;
    
        match self {
            E::Fahrenheit(f) => E::Celcius(Celcius(todo!("math"))),
            E::Celcius(f) => E::Fahrenheit(Fahrenheit(todo!("math"))),
        }
    }
}

This proposal would allow omitting the use Temperature as E; line.

@Fishrock123
Copy link
Contributor

Fishrock123 commented Aug 30, 2021

Ah I think I understand better now - the _ would be scoped to the type in the match. I agree this (or something like it) would be very nice!

@leonardo-m
Copy link

If no significant troubles will be found with this syntax, I think this idea will eventually be implemented in Rust. It's sufficiently natural, it doesn't increase the amount of Rust grammar/syntax to remember, and in some situations it removes useless redundancy. A disadvantage is that in some cases it's less obvious for the person that reads the code what the enum is. So, as usual, common sense is required to the programmer to avoid puzzle-style coding.

@jhpratt
Copy link
Member

jhpratt commented Nov 2, 2021

Just for reference I'm starting to write an RFC that would permit this in patterns (nb: not construction).

@GoldsteinE
Copy link

@jhpratt Hi! Are you still working on that RFC? I’m willing to try to write this if nobody is doing it currently.

@berkus
Copy link

berkus commented Nov 30, 2021

This looks very convenient indeed!

@jhpratt
Copy link
Member

jhpratt commented Nov 30, 2021

@GoldsteinE go for it!

@Fishrock123
Copy link
Contributor

Sorry to be another person on this, but I have a lot of free time right now and also past RFC experience...

@GoldsteinE are you still working on this or can I pick it up?

Did anyone write anything down yet?

@GoldsteinE
Copy link

Hi! Sorry, I too got distracted from writing this (and also I don’t have RFC experience yet). @Fishrock123 you can take it.

@Fishrock123
Copy link
Contributor

Pre-RFC up on the internals forum: https://internals.rust-lang.org/t/pre-rfc-inferred-enum-types/16100

@8573
Copy link

8573 commented Feb 15, 2022

Circa 2020 I was in favor of this, but now I think this trades too much explicitness for too little added convenience compared to the existing option of the following:

use Direction as D;
match dir {
  D::North => { ... }
  D::East => { ... }
  D::South => { ... }
  D::West => { ... }
}

@berkus
Copy link

berkus commented Feb 16, 2022

Circa 2020 I was in favor of this, but now I think this trades too much explicitness for too little added convenience compared to the existing option of the following:

use Direction as D;
match dir {
  D::North => { ... }
  D::East => { ... }
  D::South => { ... }
  D::West => { ... }
}

sooo....

match dir in Direction {
    _::North => { .. }
    _::East => { .. }
    _::South => { .. }
    _::West => { .. }
}

@rami3l
Copy link
Member

rami3l commented Feb 16, 2022

Circa 2020 I was in favor of this, but now I think this trades too much explicitness for too little added convenience compared to the existing option of the following:

use Direction as D;
match dir {
  D::North => { ... }
  D::East => { ... }
  D::South => { ... }
  D::West => { ... }
}

sooo....

match dir in Direction {
    _::North => { .. }
    _::East => { .. }
    _::South => { .. }
    _::West => { .. }
}

Looks like you just want a syntax sugar of the following:

{
  use Direction::*;
  match {
    North => { .. }
    East => { .. }
    South => { .. }
    West => { .. }
  }
}

... and that takes us back to #2830.

@jhpratt
Copy link
Member

jhpratt commented Feb 16, 2022

There's other places than match, though. What about if let Some(_::East) = maybe_dir {}? Generalizing to anywhere patterns are permitted seems reasonable.

@berkus
Copy link

berkus commented Feb 17, 2022

@rami3l yes, indeed!

@Fishrock123
Copy link
Contributor

Fishrock123 commented May 10, 2022

(if it wasn't clear: I gave up on this after the very large pushback on the internals forum, maybe someone can come at it with a better argument or angle.)

@jhpratt
Copy link
Member

jhpratt commented May 11, 2022

I still plan on an RFC that would permit it in pattern matching (but not construction/literals). When I brought it up on IRLO a while back there was general support behind that.

@ericsampson
Copy link

That sounds great @jhpratt. Plus you can steal a lot of the pre-RFC text that Fishrock wrote 😁

@jrandolf
Copy link

jrandolf commented Feb 4, 2024

Just a wild thought. What would prevent us from proposing

match dir {
  North => { ... }
  East => { ... }
  South => { ... }
  West => { ... }
}

without the _ like in Swift? FWIU about the compiler, the match already knows the type of dir, so it knows the variants. I assume the most complex part is implementing the compat: if any of the variants correspond to an already imported type, the imported type can take priority.

@Fishrock123
Copy link
Contributor

Fishrock123 commented Feb 5, 2024

without the _

I actually also mentioned this in a recent Mastodon discussion with Yosh - https://mastodon.social/@Fishrock/111817810166819135

The rest of the thread should also be visible there. Chris was thinking that this wouldn't be possible due to existing typename collision but I think there should be some way to not make that be an issue.

Additionally, this is already a warning:

fn main() {
    let a = std::time::Duration::from_secs(1);
    
    match a {
        Duration => println!("{:?}", Duration)
    }
}

I'm fairly certain that the compiler should be able to just internally prefix unknown types in matches to enum variants if:

  • the match is over an enum
  • the name in the match arm is the name of a varient of the enum

I don't really see what could go wrong there. We already know that the match has to be variants of the enum in some form if we are matching it. There could be a some trouble if the enum type is not already known somehow (maybe by way of an intermediate .into() conversion), but I am sure we could find a way to have a good error message for that.

Am i missing something? It does not seem that complicated to just omit the enum name altogether and infer it.

@afetisov
Copy link

What would prevent us from proposing ... without the _ like in Swift?

Too error-prone. If you make a typo, or remove a variant from the enum but not the match, then you'll get incorrect behaviour, because Rust would treat the corresponding branch as a catch-all pattern binding the value to a new identifier. This is already the case if try to match against constants and make a typo/ Protecting against this kind of error is one of the motivations to always write constants in ALL_CAPS, because it's easier to notice when you create a new variable instead.

A major benefit of Rust's pattern matching is that you can be generally sure that the compiler has your back, catching any discrepancy between the definition and use of enum variants. If you add, remove or change any variants, the compiler will complain, making refactorings much more reliable (unless you overuse catch-all arms). Anything which decreases that reliability is thus undesirable.

@Fishrock123
Copy link
Contributor

Too error-prone. If you make a typo, or remove a variant from the enum but not the match, then you'll get incorrect behaviour, because Rust would treat the corresponding branch as a catch-all pattern binding the value to a new identifier. This is already the case if try to match against constants and make a typo/ Protecting against this kind of error is one of the motivations to always write constants in ALL_CAPS, because it's easier to notice when you create a new variable instead.

I don't think this is a very solid argument against it; as I pointed out above Rust will already emit a warning in that case, and this same problem would be present with code like:

{
    use MyEnum::*;
    match my_enum {
       Variant => {}
    }
}

@scottmcm
Copy link
Member

scottmcm commented May 20, 2024

To add to the previous point, rust now has a dedicated deny-by-default lint for exactly the "binding name matches a variant name" case because it's so unlikely to be what was wanted: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=145cb74ce232c7430e5368c799dd1c39

error[E0170]: pattern binding `Less` is named the same as one of the variants of the type `std::cmp::Ordering`
 --> src/lib.rs:3:12
  |
3 |     if let Less = x {
  |            ^^^^ help: to match on the variant, qualify the path: `std::cmp::Ordering::Less`
  |
  = note: `#[deny(bindings_with_variant_name)]` on by default

(I'm still not a fan of doing this with no syntax, though. .Less might be fine, but just Less I'm skeptical.)

@lemon-gith
Copy link

lemon-gith commented Jun 24, 2024

Just a thought, but what if this form of syntax were to be introduced:

match dir using Foo::Bar::Direction {
    North => { ... }
    East => { ... }
    South => { ... }
    West => { ... }
}

where using implicitly scopes Foo::Bar::Direction::*, for ease of use?

  • I don't think people will mind if the line with match gets a little longer
  • This allows for narrower scoping than a preceding use statement
  • I'm impartial to the exact keyword used, but using is somewhat akin to use and so identifies the usage more clearly?
    • @berkus brings up the possible use of in instead, which I also find quite intuitive

However, this comment on #2830 brings up the very valid point of match statements that destructure composite types, and hence different (possibly conflicting) type-names.
For which I'd like to offer an extension to the syntax, to mirror type-specification in function declarations:

match (fruit, company) using (
    fruit: Foo::Fruits,
    company: Bar::Companies
) {
    (Apple, Google) => { ... }
    (Orange, Samsung) => { ... }
    (Durian, Apple) => { ... }
    _ => { ... }
}

This is another reason I would prefer a keyword like using, because it lends itself well to pluralisation.
But, I do understand that it's probably annoying to have to add a new keyword to the lexer, especially with this strange extended syntax that the parser will have to make heads or tails of.

I might be very wrong, but as far as I can tell, any super deep nesting would require either:

  • manual construction of a complicated scrutinee
    • in which case, the one that constructed that scrutinee can also construct a complicated type specification :)))
  • existing types to pull from
    • which the compiler should(?) be able to infer, since those types will have to have been explicitly defined somewhere

Well these are my two cents on the matter (ok, a bit more than two). Like most other programmers I dislike writing more code than I need to, but I also appreciate both explicit and strong typing, and the power that that affords match statements; thus, I feel that being able to scope a type just where you need it would be a great solution to the verbosity. This is an idea that borrows from a great many other ideas, but I would love to see something added to the language to address this, either way.
And for anyone that's read all my ramblings, thank you, I appreciate it, have a lovely day ^-^

Edit: I think I've made a mistake in the second example, because I'm just coming up with constructed examples, but I hope the general point still stands.

@igotfr
Copy link

igotfr commented Jan 10, 2025

Circa 2020 I was in favor of this, but now I think this trades too much explicitness for too little added convenience compared to the existing option of the following:
use Direction as D;
match dir {
D::North => { ... }
D::East => { ... }
D::South => { ... }
D::West => { ... }
}

sooo....

match dir in Direction {
    _::North => { .. }
    _::East => { .. }
    _::South => { .. }
    _::West => { .. }
}
match dir as Direction {
     _::North => { .. }
     _::East => { .. }
     _::South => { .. }
     _::West => { .. }
}

@kennytm
Copy link
Member

kennytm commented Jan 10, 2025

for the actual rfc please discuss in #3444.

@igotfr
Copy link

igotfr commented Jan 11, 2025

is it possible infer this way?

match dir as Direction {
     ::North => { .. }
     ::East => { .. }
     ::South => { .. }
     ::West => { .. }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests