HiveBrain v1.2.0
Get Started
← Back to all entries
patternrustModerate

Unwrap inner type when enum variant is known

Submitted by: @import:stackoverflow-api··
0
Viewed 0 times
enumknownunwrapvariantinnerwhentype

Problem

I have this enum type:

enum Animal {
    Dog(i32),
    Cat(u8),
}


Now I have a function that takes this type as parameter. I know (for some reason) that the input is always a Cat. I want to achieve this:

fn count_legs_of_cat(animal: Animal) -> u8 {
    if let Animal::Cat(c) = animal { c } else { unreachable!() }
}


Can I write this shorter and/or more idiomatic?

Solution

What I have seen is to introduce a new struct for each enum variant, and then methods on the enum to decompose it:
struct Dog(i32);
struct Cat(u8);

enum Animal {
Dog(Dog),
Cat(Cat),
}

impl Animal {
fn cat(self) -> Cat {
if let Animal::Cat(c) = self {
c
} else {
panic!("Not a cat")
}
}

fn dog(self) -> Dog {
if let Animal::Dog(d) = self {
d
} else {
panic!("Not a dog")
}
}
}

// Or better an impl on
Cat ?
fn count_legs_of_cat(c: Cat) -> u8 {
c.0
}


You don't need the struct as you could just return the u8, but that may get hard to keep track of. If you have multiple variants with the same inner type, then it would potentially be ambigous.

Over the years, there have been a number of RFCs to provide language support for this (a recent one being RFC 2593 — Enum variant types). The proposal would allow enum variants like Animal::Cat to also be standalone types, thus your method could accept an Animal::Cat directly.

I almost always prefer to write the infallible code in my inherent implementation and force the caller to panic:
impl Animal {
fn cat(self) -> Option {
if let Animal::Cat(c) = self {
Some(c)
} else {
None
}
}

fn dog(self) -> Option {
if let Animal::Dog(d) = self {
Some(d)
} else {
None
}
}
}


I'd probably use a match:
impl Animal {
fn cat(self) -> Option {
match self {
Animal::Cat(c) => Some(c),
_ => None,
}
}

fn dog(self) -> Option {
match self {
Animal::Dog(d) => Some(d),
_ => None,
}
}
}


Since Rust 1.34, I'd use the TryFrom trait in addition to or instead of the inherent implementations:
impl TryFrom for Cat {
type Error = Animal;

fn try_from(other: Animal) -> Result {
match other {
Animal::Cat(c) => Ok(c),
a => Err(a),
}
}
}

impl TryFrom for Dog {
type Error = Animal;

fn try_from(other: Animal) -> Result {
match other {
Animal::Dog(d) => Ok(d),
a => Err(a),
}
}
}


Consider using a dedicated error type that implements std::error::Error instead of directly returning the Animal in the failure case. You may also want to implement From to go from Cat / Dog back to Animal.

This can all get tedious, so a macro can be of good use. I'm sure there are plenty of good crates out that that do this, but I often write my own one-off solution:
macro_rules! enum_thing {
(
enum $Name:ident {
$($Variant:ident($f:ident)),* $(,)?
}
) => {
enum $Name {
$($Variant($Variant),)*
}

$(
struct $Variant($f);

impl TryFrom for $Variant {
type Error = $Name;

fn try_from(other: $Name) -> Result {
match other {
$Name::$Variant(v) => Ok(v),
o => Err(o),
}
}
}
)*
};
}

enum_thing! {
enum Animal {
Dog(i32),
Cat(u8),
}
}

Context

Stack Overflow Q#34953711, score: 47

Revisions (0)

No revisions yet.