How to impl `From` a generic error into my own?

I'm using the num_enum crate to convert enum variants into/from a primitive (I don't know why Rust doesn't provide this out of the box).

The problem is that it returns a generic error TryFromPrimitiveError<Enum: TryFromPrimitive>. Is it possible to convert a generic error into my own?

I'm currently using map_err everywhere but the code becomes very messy.

Preferably I would want something like this (doesn't compile):

use num_enum::{TryFromPrimitive, TryFromPrimitiveError};

#[repr(u8)]
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
enum Foo {
  A = 1,
  B = 2,
  C = 3,
}

#[repr(u8)]
#[derive(Debug, PartialEq, Eq, TryFromPrimitive)]
enum Bar {
  X = 4,
  Y = 5,
  Z = 6,
}

#[derive(Debug)]
enum MyError {
  EnumError(TryFromPrimitiveError<Enum>),
}

impl From<TryFromPrimitiveError<Enum>> for MyError {
  fn from(error: TryFromPrimitiveError<Enum>) -> Self {
    MyError::EnumError(error)
  }
}

fn main() -> Result<(), MyError> {
  let b = Foo::try_from(2)?;
  assert_eq!(b, Foo::B);

  let wrong = Bar::try_from(8)?;

  Ok(())
}

Becuase it doesn't work except in the simplest of cases. Such specialized behaviour is best left to third party crates, such as num-enum.

As for your actual problem, you can do the following:

  • Return Result<(), Box<dyn Error>> from your function.
  • Use anyhow for your error type.
  • Write a converter function:
fn to_my_error<Enum: TryFromPrimitive>(lib_err: TryFromPrimitiveError<Enum>) -> MyError {
    // Fill in the logic
}

From your main it looks like you don't want MyError to be generic? In that case, you're going to have to drop or type-erase the Enum somehow. The most straight-forward way is probably to have

enum MyError {
    EnumError(Box<dyn Error /* + Send + Sync + 'static*/>),
}

(You can't have a TryFromPrimitiveError<Box<dyn TryFromPrimitive>> to type-erase the Enum parameter directly as TryFromPrimitive is not a dyn-safe trait.)

Thanks for the reply! Yesterday I opened an issue on the num_enum repo:

and I was given a pretty nice solution:

impl<Enum: TryFromPrimitive> From<TryFromPrimitiveError<Enum>> for YourError {
    fn from(err: TryFromPrimitiveError<Enum>) -> Self {
        /* the contents of your current `.map_err()` */
    }
}

It's just what I needed. I didn't remember that impl can take generic parameters!

Another crate to look at is strum, which provides a FromRepr trait in the cases where num_enum fails requires a Default impl.

FWIW Rust does allow converting simple enums like the ones you mentioned into primitives out of the box, with as.

Using the definitions above for Foo and Bar:

fn main() {
    assert_eq!(Foo::A as u8, 1);
    assert_eq!(Bar::X as u8, 4);
}

Playground Link

Note that this will not work for enums like this:

enum Baz {
  Foo(Foo),
  Bar(Bar),
}
error[E0605]: non-primitive cast: `Baz` as `u8`
  --> src/main.rs:28:10
   |
28 |     dbg!(Baz::Foo(Foo::A) as u8);
   |          ^^^^^^^^^^^^^^^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

For more information about this error, try `rustc --explain E0605`.

strum looks interesting, but it returns Option instead of Result and, from what I can tell, you can't specify a default variant in order to have an infallible function like in num_enum

That's true. But, from what I read in the num_enum's page, doing it that way can be not type safe?

Quoting that page:

num_enum ’s IntoPrimitive is more type-safe than using as , because as will silently truncate - num_enum only derives From for exactly the discriminant type of the enum.

So I suppose they are talking about how if you had an enum that grew to be too large to fit into a u8, or otherwise had a variant that had a large value, then old casting to u8 would appear to work, but would truncate the value. Like this:

enum E {
    NotLarge = 44,
    Large = 300,
}

fn main() {
    assert_ne!(E::Large as u8, E::NotLarge as u8); // fails
}

I suppose that could be a concern some of the time, depending on how likely the domain model is need values outside the initial range, and how hard it would be to update all the usage code in that case.

One thing that could be done, which does not technically improve type safety, but makes mistakes easier to spot, is to use a type alias like this:

enum E {
    NotLarge = 44,
    Large = 300,
}

type EPrimitive = u16;

fn main() {
    assert_ne!(E::Large as EPrimitive, E::NotLarge as EPrimitive);
}

Probably better than just doing that though would be to implement From using as, and use that instead.

impl From<E> for EPrimitive {
    fn from(e: E) -> Self {
       e as Self
    }
}

fn main() {
    assert_ne!(EPrimitive::from(E::Large), EPrimitive::from(E::NotLarge));
}

That way, if you change type EPrimitive = u8; to type EPrimitive = u16; things work properly.

1 Like

FWIW, I recently needed to split up one #[repr(u8)] enum, into a few different enums, there was some concern that working with raw primitives wires would get crossed.

I ended up building a generalized newtype OpaqueRepr<T>(T), and deriving associated type

trait EnumMetadata<T> {
  type Repr: lots of num_traits
}

I haven't released it to crates.io yet as I implemented it using some upstream macros which haven't landed.
GitHub - ratmice/enum_extra: Derive macros and Traits for working with enum reprs.

    #[derive(EnumMetadata, Debug, Eq, PartialEq)]
    enum Foo {
        Bar = 1 << 0,
        Baz = 1 << 1,
    }

    let repr: OpaqueRepr<Foo> = Foo::Bar.opaque_repr() | Foo::Baz;

It goes a bit beyond that in also having NonZero versions of reprs which uses constant evaluation to
turn things like the following into compilation failures.

#[derive(NonZeroRepr, EnumMetadata)]
#[repr(u8)]
enum A {
    A = 0 << 1,
}

Anyhow, I was really happy with this associated type/constant expr/generic monstrosity.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.