Generic referencing enum inner data

I've reached a point where I've been trying to add common data to a particular enum among it's variants, and discovered a limitation that had not previously occurred to me. There's no way (without a macro I suppose) to get at the inner data of an enum value, unless the variant is known. Is that correct?

While the goal behind the following code is obvious, it will not compile:

(I understand positional references only work with tuples--- that's not my point, they could also "in theory at least" refer to elements in an enum that share a common type.)

#[derive(Debug)]
enum Foo {
    Bar(u32),
    Bink(u32),
}
use Foo::*;

fn main() {
    let bar = Bar(10);
    let bink = Bink(20);
    
    println!("bar={:?}, bink={:?}", bar, bink);
    
    println!("bink num = {}", bink.0);
}

The error message is:

error[E0609]: no field `0` on type `Foo`
  --> src/main.rs:14:36
   |
14 |     println!("bink num = {}", bink.0);
   |    

What's interesting about the error message is that it would seem to imply there is a way to attach a field named 0 to an enum. But as far as I know this is not possible (is it?)

For example, why doesn't the message say "enums cannot have fields?" instead of "Foo doesn't have a field named xyz?

At any rate. It would be nice, if, when all the variants of an enum share an identical type, that there were a way to refer to that data by it's position instead of having to pull it would with some long match statement, etc...

Of course the architecture could be modified to have an outer 'struct" that holds the data shared among the variants, however that would seem to add unnecessary complexity.

There are essentially two ways to do something like this:

impl Foo {
    fn get_value(&self) -> u32 {
        match self {
            Bar(value) => *value,
            Bink(value) => *value,
        }
    }
}
struct Foo {
    value: u32,
    kind: FooKind,
}

enum FooKind {
    Bar,
    Bink,
}

It didn't say "enums cannot have fields" because you did not try to define a field for Foo. I don't think that error message makes sense for trying to access a field at all - it only makes sense if you were trying to define a field.

4 Likes

Alice,

Thank you for your reply. Alas, I already knew about the option you propose. In my quote from that message:

By "some long match statement", I meant I wished to avoid having to list each possible variant. Imagine if there were many variants. Having to write a long match statement, especially having to go back and add variants to it each time one was added, is something I wish to avoid.

My point is that if as long as I stick to a "contract" such that I will always keep the first position a u32 ( for example ) it would be nice if the compiler permitted a generic way to refer to data values that occupy this position.

Regarding the compiler error:

error[E0609]: no field `0` on type `Foo`

The error message to me means at some level the compiler took my expression

bink.0

To mean a field named 0 as a member of the value stored in bink. I'm not making a big deal about the wording of the error, it's just interesting to me that at some level, if only the contents of bink being that it's of type Foo could be seen as a tuple wrapped by a variant (as the message implies) this would be nice. ( And, further the variant in my case could be somehow "wildcarded". )

For example it would be nice if you could write:

let num = match bink { _(num) => num };

And have the don't care symbol ('_') refer to the variant's functor itself as a holder of num something which could be sought after.

Okay well, the answer is that you can't do this.

I was afraid of that. (I'm also assuming there's no way to add a field to an enum.)

Something like this:

enum Foo {
     Bar(u8),
     Bink(u8),
     data:  u32,
     other_data: String,
}

And all varants would share the named data just as though it were a struct.

That would be another cool thing!!!

An interesting point is that such a thing could simply be called a struct and there would be no need for enums at all. What is called an enum today would simply be a struct that has one or more variants.

Note that or-patterns can also be useful here, they can even be used in let it they're exhaustive

e. g.

let (Bar(num) | Bink(num)) = x;

(playground)

I suppose, you could even define a macro just for that pattern if the enum is long.

2 Likes

There are ways to get well-defined layouts of enums, but you would need unsafe to exploit that. I agree it'd be nice if Rust grew some more utilities for "homogenous enums".

If your payload is a type instead of an unnamed collection of fields, there are some crates that can help, e.g. by deriving some sort of getter trait.

1 Like

Thanks for the tip on using the | operator that way.

I'm not up on macro writing. Could a macro be written that would expand to cover all variants so that the user code would not require listing them out?

I'm thinking outloud here:

        scan_enum!("_({})", foo, num);
        scan_enum!("_({},_,{})", bar, val1, val2);

I was thinking of a macro like

macro_rules! Foo {
    ($p:pat) => {
        Foo::Bar($p) | Foo::Bink($p)
    }
}

or perhaps something like

enum Baz {
    Variant1 {
        x: u32,
        y: String,
        z: bool,
    },
    Variant2 {
        x: u32,
        z: bool,
        qux: fn(),
    }
}

macro_rules! Baz {
    ($($field:ident $(: $p:pat)?,)* ..) => {
        Baz::Variant1{ $($field $(: $p)?,)* .. } | Baz::Variant2{ $($field $(: $p)?,)* .. }
    }
}

(playground)

Maybe a macro to create such a macros could even be useful, too, so you can just write

#[derive(Debug)]
enum Baz {
    Variant1 {
        x: u32,
        y: String,
        z: bool,
    },
    Variant2 {
        x: u32,
        z: bool,
        qux: fn(),
    }
}

define_enum_macro!(Baz, Variant1, Variant2);

fn demonstration2() {
    let x = Baz::Variant1 { x: 42, y: "hello".into(), z: false };
    let Baz!{ z, .. } = &x;
    println!("{:?} with `z` being {:?}", x, z);
}

(playground)


Note that it’s usually easier to just store a common field outside of the enum; the only downside I’m aware of is that that’s sometimes resulting in less compact type layout (but that’s arguably a problem that might be fixable in different ways, too), other than that it should be equivalent. I’m referring to the approach

struct Foo {
    value: u32,
    kind: FooKind,
}

enum FooKind {
    Bar,
    Bink,
}

that @alice mentioned above.

2 Likes

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.