I don't have the time or energy to defend this as a fleshed-out (pre-)RFC so my responses will be limited. I'm mostly just posting this here in case someone else sees merit in this concept and wants to see this through. While I have had a number of use-cases for something like this, I've only done cursory research into the design space and I don't care to exhaustively discuss alternatives.
Motivation
When writing low-level protocol-handling code, it's common to have a C-like enum to represent different message or packet types, e.g.:
#[repr(u8)]
enum PacketKind {
Foo = 0,
Bar,
Baz
}
However, it then becomes tedious to use that in parsing code:
pub fn decode_packet(packet: &[u8]) -> Result<Packet, PacketError> {
if packet.len() < 2 {
return Err(PacketError::TooShort);
}
let kind = match packet[0] {
0 => PacketKind::Foo,
1 => PacketKind::Bar,
2 => PacketKind::Baz,
other => return Err(PacketError::unexpected_kind(other)),
};
match kind {
PacketKind::Foo => decode_foo(&packet[1..]),
PacketKind::Bar => decode_bar(&packet[1..]),
PacketKind::Baz => decode_baz(&packet[1..]),
}
}
Now, crates exist that let you generate a TryFrom
impl with a derive, such as num_enum
or enum-utils
. And that would let you skip writing the first match
.
However, using this in the above code effectively generates a redundant match
, as there will be one in the TryFrom
impl to convert from u8
to PacketKind
, and then one in the code we write to dispatch on the value of PacketKind
. Will the optimizer eliminate the redundant match
? Probably, but as the Rust community has been learning, it's better to not generate as much redundant code in the first place.
It's also not super easy to discover these crates; even if you search "enum repr" on crates.io, you have to sift through a number of implementations of varying quality and maintenance status: https://crates.io/search?q=enum%20repr
Proposal
Allow using C-like enum variants in match arms for their #[repr]
type:
match packet[0] /* : u8 */ {
PacketKind::Foo => decode_foo(&packet[1..]),
PacketKind::Bar => decode_bar(&packet[1..]),
PacketKind::Baz => decode_baz(&packet[1..]),
other /* : u8 */ => Err(PacketError::unexpected_kind(other)),
}
This would effectively desugar to something like:
match packet[0] /* : u8 */ {
const { PacketKind::Foo as u8 } => decode_foo(&packet[1..]),
const { PacketKind::Bar as u8 } => decode_bar(&packet[1..]),
const { PacketKind::Baz as u8 } => decode_baz(&packet[1..]),
other /* : u8 */ => Err(PacketError::unexpected_kind(other)),
}
though that's likely not valid syntax in the first place, hopefully you get the idea.
Having discussed this briefly with coworkers, we identified a few edge cases (not an exhaustive list):
- The catchall arm should be required to handle values that aren't a valid enum variant, unless
the enum exhaustively covers the value space of its
#[repr]
(feasible for 8 and 16 bit enums). - Obviously, enums with data wouldn't work with this so they should be rejected.
- We're not sure how this would interact with enum-variants-as-types but C-like enums aren't very useful with that feature anyway.
- I think you should be allowed to use multiple enum types in the same
match
as long as they have the same#[repr]
, e.g.:
#[repr(u8)]
enum SomeOtherPacketKind {
FooBar = 4,
FooBaz,
BarBaz,
}
match packet[0] {
PacketKind::Foo => decode_foo(&packet[1..]),
...
SomeOtherPacketKind::FooBar => decode_foo_bar(&packet[1..]),
other /*: u8 */ => Err(PacketKind::unexpected_kind(other)),
}
though I do concede that this code kind of has a smell to it, so I wouldn't be against having a lint for this. If the enums overlap that should be fine as they can be linted just like other redundant match arms.
- Binding patterns should be allowed, but if mixing and matching is allowed then obviously you can't do that within a binding pattern because there's no applicable type without anonymous sum types:
match packet[0] /* : u8 */ {
// Allowed
kind /* : PacketKind */ @ (PacketKind::Foo | PacketKind::Bar) => decode_foo_or_bar(&packet[1..]), kind),
// Rejected
kind /* : ??? */ @ (PacketKind::Foo | SomeOtherPacketKind::FooBar) => (),
...
}