derive(TryFrom) for C-like enums

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:

  1. could be solved by injecting the type of the enum and using core::any::type_name.
  2. could be solved by requiring EnumTryFromError to be a ZST with no alignment requirements and using core::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 reprs 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

4 Likes

There is ::num_enum, which does support all these features (and, optionally, even more features), and is actively maintained (disclaimer: I help co-maintain that crate).

Its only true issue is discoverability, which I guess could be improved through some nurseries / rust-unofficial publicity.

repr(Rust) enums should be free of any integer-layout constraint, so I wouldn't expect that conversion to be available even if this TryFrom was built-in.

I think the current requirement of num_enum is quite honest: the enum needs an explicit #[repr({integer})] annotation:

#[macro_use] extern crate num_enum;

#[derive(TryFromPrimitive)] // and you can even add IntoPrimitive for non-truncating back conversion (contrary to the dangerous `as` casts)
#[repr(i32)] // Ok, programmer explicitly opted into using `i32` for its backing integer
pub enum Version {
    American = 18,
    European = 19,
}

fn main ()
{
    let version = Version::try_from(18).unwrap();
}

I suggest infecting the error type with the type of the enum, so as to mention it in the pretty-printed description of the error message:

#[derive(...)]
pub struct EnumTryFromError<Enum : TryFromPrimitive> {
    pub invalid_discriminant: <Enum as TryFromPrimitive>::Primitive,
    // (and phantomdata)
}

impl<Enum : TryFromPrimitive> fmt::Display for EnumTryfromError<Enum> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use fmt::Write;
        write!(f,
            "`{:?}` is not a valid discriminant for the type {}",
            self.invalid_discriminant,
            ::core::any::type_name::<Enum>(),
        )
    }
}
3 Likes

Regarding integer types, you can just provide an implementation of TryFrom for all integer types.

That is excellent! I will add it to the original post.

That is also an excellent point. I've already updated the original post to reflect that.

I had the same idea!

1 Like

Yes, please! This is a very common problem. People keep reinventing crates for this!

4 Likes

I've opened an issue for this before: https://github.com/rust-lang/rfcs/issues/2783

I'd be surprised if it's the only one.