Allow inferring enum variant types with `_::Variant` syntax
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);
You should already be able to import enum variants directly:
use CompassPoint::West
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.
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!
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.
Just for reference I'm starting to write an RFC that would permit this in patterns (nb: not construction).
@jhpratt Hi! Are you still working on that RFC? I’m willing to try to write this if nobody is doing it currently.
This looks very convenient indeed!
@GoldsteinE go for it!
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?
Hi! Sorry, I too got distracted from writing this (and also I don’t have RFC experience yet). @Fishrock123 you can take it.
Pre-RFC up on the internals forum: https://internals.rust-lang.org/t/pre-rfc-inferred-enum-types/16100
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 => { ... }
}
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 => { .. }
}
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 https://github.com/rust-lang/rfcs/issues/2830.
There's other places than match, though. What about if let Some(_::East) = maybe_dir {}? Generalizing to anywhere patterns are permitted seems reasonable.
@rami3l yes, indeed!
(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.)
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.
That sounds great @jhpratt. Plus you can steal a lot of the pre-RFC text that Fishrock wrote 😁
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.
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
matchis over anenum - 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.
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.
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 => {}
}
}