Preamble
Every so often either me or someone I know runs into a case like this (taken from real conversations I've had):
"is it possible to convert an i32 into the enum value of the same discriminant? as in i want to do this"
pub enum Version {
American = 18,
European = 19,
}
fn main() {
let version: Version = 18.into();
}
And then I begrudgingly tell them:
"you have to implement TryFrom<i32> for Version
, which would look like this"
struct InvalidVersion(i32);
impl std::convert::TryFrom<i32> for Version {
type Error = InvalidVersion;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
18 => Ok(Version::American),
19 => Ok(Version::European),
_ => Err(InvalidVersion(value))
}
}
}
"or, use a newtype, but keep in mind you will have to add checks for validity elsewhere:"
struct Version(i32);
impl Version {
const AMERICAN: i32 = 18;
const EUROPEAN: i32 = 19;
}
let v = Version::EUROPEAN;
I find neither of these solutions particularly satisfactory, seeing as this is something that keeps coming especially with people learning Rust for the same time. It scales especially poorly to enums with dozens or maybe even hundreds of variants, which even with macros are hard to make look nice.
Existing Solutions
Crates like conv, which have been around since as early as Rust 1.2, have attempted to mitigate this.
However, particularly with conv
, I see several problems:
- It is too broad, providing a lot of conversions that would either be considered unidiomatic, or are simply too specific to be useful
- It has not been updated since 2016, and as such cannot make use of any of the changes to the standard libraries or the macro system.
- People typically tend to hit this nag a while before they learn about Rust's package ecosystem, and telling them to either "deal with it" or "use this (ancient) crate" is not what they want to hear coming into a new language.
UPDATE:
I was made aware of the num_enum
crate, which is exactly made to cover this use case, and is actively maintained.
Proposal
I would like to propose adding support for derive(TryFrom)
for C-like enums to the Rust core libraries.
Such that the following would work without the need for any external crates:
#[derive(TryFrom)]
enum Version {
American = 18,
European = 19,
}
assert_eq!(Version::try_from(19), Ok(Version::European));
Alongside this, a new error type would be added to the core::convert
module, which would function similarly to CharTryFromError
:
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct EnumTryFromError(());
impl fmt::Display for EnumTryfromError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"converted integer not representable by enum".fmt(f)
}
}
Though this is likely subject to change, see below.
Open Questions
Error Type(s)
There are two major problems with EnumTryFromError
as proposed above:
-
It does not, and cannot, name the concrete type for which the conversion failed. This is fine for something like
TryFromIntError
, as all integer types represent the same thing, but inadequate for enum types. -
As derived trait implementations reside in user code,
EnumTryFromError
would need to be externally constructible, which would run counter to other "TryFromError" types from the core libs.
UPDATE:
- could be solved by injecting the type of the enum and using
core::any::type_name
. - could be solved by requiring
EnumTryFromError
to be a ZST with no alignment requirements and usingcore::mem::transmute(())
. While admittedly a hack, derive macros are supposed to be fully opaque to the user, so this would not be a problem.
I do not think having the derive
emit a specialized form of EnumTryFromError
would be possible, as we would risk namespace pollution.
A more general error type which would be deliberately constructible (like io::Error
) could be possible, but would be subject to more bikeshedding, and, more importantly, could potentially make for even less descriptive error reporting.
Choice of integer types
How should an appropriate integer type be chosen, in the presence (or absence) of repr
attributes? Does a proc_macro_derive
even have access to this information?
What should the default be (i.e. repr(Rust)
)?
UPDATE:
An explicit repr(inttype)
would be necessary, as repr(C)
is "an educated guess" and repr(rust)
is "assume nothing about anything". Other repr
s do not apply to C-like enums.
Is there a better way?
The potential issues of adding a derive
macro for something fairly specific are quite apparent.
However, it is equally apparent that this is a frequent enough point of annoyance that it warrants discussion.
What would be alternative ways to address this use case?
Perhaps a solution more akin to what bitflags
did could be more appropriate?
UPDATE:
I wanted to convince myself of the possibility of this, so I wrote a prototype with macro_rules!
: GitHub Gist