Generic types in enum variants

Hey there,

I needed a very large enum lately, while creating it I wondered why are there no generic types in enum variants?

Obviously you can define global types, but this always leads to different, non compatible types.

My concept of such an enum would look like this:

pub enum MyEnum {
    TupleLikeVariant<T: Clone + Debug>(T),
    StructLikeVariant<S: Clone> {
        content: S
    },
    NonGenericVariant(u32)
}

This enum could be initialized just like every other enum aswell:

pub fn main() {
    let v1: MyEnum = MyEnum::TupleLikeVariant::<String>(String::from("generic string variant"));
    let v2: MyEnum = MyEnum::StructLikeVariant::<usize>{ content: 123usize };
    let v3: MyEnum = MyEnum::TupleLikeVariant(1.5);
}

Obiously you should be able to switch by patterns:

impl Clone for MyEnum {
    fn clone(&self) -> Self { 
        match self {
            MyEnum::TupleLikeVariant<T>(value) => {
                MyEnum::TupleLikeVariant(value.clone())
            }
            MyEnum::StructLikeVariant {content: value} => {
                MyEnum::StructLikeVariant {
                    content: value.clone()
                }
            }
            MyEnum::NonGenericVariant(value) => MyEnum::NonGenericVariant(value.clone())
        }
    }
}

and use it in if clauses:

pub fn print_it(enum_value: MyEnum) {
    if let MyEnum::TupleLikeVariant::<T>(value) = enum_value {
        println!("{:?}", enum_value);
    }
}

the only problem I've come up with is defining explizit representation values to the enum value:

pub enum MyEnum {
    TupleLikeVariant<T: Clone + Debug>(T) = 42;
}

which is obviously not possible due to multiple implementation of one variant. But I guess this could be solved.

Is there any other reason why it would not work and why it is not implemented?

The issue is how would this be represented in memory. MyEnum would be one type, with one representation, but its variants may contain any type T.

1 Like

Yeah thats the idea. The inner type is at every point clearly defined by the variant of the enum. Therefore the compiler should be able to select the actual generated implementation. Or am I missing something?

In the end it is just syntactic sugar for multiple same implementations of different types.

That's generally only known at runtime, therefore at compile time the compiler must generate code that handles all possible variants, but this is not possible in your proposal.

What you're describing here is more like the following:

pub enum MyEnum<T: Clone + Debug> {
    TupleLikeVariant(T),
}

If you declare the generic on the variant then you want one type and implementation, not many different ones.

Indeed, this sounds like you want some kind of "const enum" at which point you maybe want to consider using the typestate pattern instead, if you can guarantee all types at compile time anyway.

Your type would lead to different types and I agree that it is partitialy only known at compile time, but this is already the case for enums. If you got different types in enum variants, you obviously can't select during compile time.

The compiler would generate the code for the different cases. This example:

pub enum MyEnum {
    TupleLikeVariant<T: Clone>(T)
}

pub fn main() {
    let v1: MyEnum = MyEnum::TupleLikeVariant(1usize);
    let v2: MyEnum = MyEnum::TupleLikeVariant(1u32);
    let v3: MyEnum = MyEnum::TupleLikeVariant(String::from("Some String"));
    let v4: MyEnum = MyEnum::TupleLikeVariant(15f32);
    let v5: MyEnum = MyEnum::TupleLikeVariant(-1.3f64);
}

impl Clone for MyEnum {
    fn clone(&self) -> Self { 
        match self {
            MyEnum::TupleLikeVariant<T>(value) => { 
                MyEnum::TupleLikeVariant(value.clone())
            }
        }
    }
}

would extend to something like that:

pub enum MyEnum {
    TupleLikeVariant__usize(usize),
    TupleLikeVariant__u32(u32),
    TupleLikeVariant__String(String),
    TupleLikeVariant__f32(f32),
    TupleLikeVariant__f64(f64),
}

pub fn main() {
    let v1: MyEnum = MyEnum::TupleLikeVariant__usize(1usize);
    let v2: MyEnum = MyEnum::TupleLikeVariant__u32(1u32);
    let v3: MyEnum = MyEnum::TupleLikeVariant__String(String::from("Some String"));
    let v4: MyEnum = MyEnum::TupleLikeVariant__f32(15f32);
    let v5: MyEnum = MyEnum::TupleLikeVariant__f64(-1.3f64);
}

impl Clone for MyEnum {
    fn clone(&self) -> Self { 
        match self {
            MyEnum::TupleLikeVariant__usize(value) => { 
                MyEnum::TupleLikeVariant__usize(value.clone())
            },
            MyEnum::TupleLikeVariant__u32(value) => { 
                MyEnum::TupleLikeVariant__u32(value.clone())
            },
            MyEnum::TupleLikeVariant__String(value) => { 
                MyEnum::TupleLikeVariant__String(value.clone())
            },
            MyEnum::TupleLikeVariant__f32(value) => { 
                MyEnum::TupleLikeVariant__f32(value.clone())
            },
            MyEnum::TupleLikeVariant__f64(value) => { 
                MyEnum::TupleLikeVariant__f64(value.clone())
            },
        }
    }
}

At every point it is clear which type should be handled.

The typestate pattern would lead to different types, the target is to have just different variants depending on the usage.

The compiler can't do that because crates are compiled separately. It can't know all the users of the enum in other crates.

The issue is not about monomorphization, but about layout. In your example, there is no one type MyEnum as let v1: MyEnum = suggests. Depending on the monomorphized variant, the type will have a different size and layout, which would actually make it a different type.

If you know your specific subset of types used, you can define enum MyEnum<A, B> and then implement all required functionality for all your type combinations explicitly, e.g. impl MyEnum<usize, String>. But again, at this point, an enum might have been the wrong choice anyway.

If you want a common type with variable content, you will need to use dynamic types (aka trait objects) within your variants.

I agree on that problem. Therefore I have another question: How are generics currently compiled? Lets say I got crate A with this code:

pub fn clone_value<T: Clone>(value: &T) -> T {
    value.clone()
}

and crate B with the usage:

pub fn main() {
    let v = some_clone(30usize);
}

is the actual implementation:

pub fn clone_value__usize(value: &usize) -> usize {
    v.clone()
}

In the binary of crate B?

As far as I know enums are currently represented the following way:
The alignment is equal to the largest alignment of all interior types and the representation type. Each variant is represented by first the representation, and following all interior values. Each interior value begins with the padding bytes to align the value, followed by the value itself. At the end are padding bytes to match the size of the largest variant.

Even if you would use generics this concept would work, because you know at compile time how large your largest used generic is.

But it would break appart once you want to add another type from a different crate, this might change the overall size of the enum values and all implementations from other crates would need to be modified.

Yes this might be an option, but this would be too much overhead for it. I just implement all types manually.

Yes, that's pretty much how they are compiled. But differently from your enum proposal, knowledge about all uses of the function is not needed to compile one concrete version, for example you did not need to know that clone_value might have been called with a &MyVeryBigType parameter from another crate.

Note that if your idea was possible then trait objects would not need to be !Sized and could be compiled in pretty much the same way you would expect here, but there's a reason this is not the case.

I'm confused by the "this can't be implemented" objections, because isn't this...

enum MyEnum {
    TupleLikeVariant<T: Clone + Debug>(T),
    StructLikeVariant<S: Clone> {
        content: S
    },
    NonGenericVariant(u32)
}

... isomorphic with this, which works just fine today?

enum MyEnum<T: Clone + Debug, S: Clone> {
    TupleLikeVariant(T),
    StructLikeVariant {
        content: S
    },
    NonGenericVariant(u32),
}

No because in the original proposal the type has no type parameters:

fn f(e: MyEnum)

and in your case it has parameters:

fn f(e: MyEnum<i32, i32>)

You modified the proposal to write the type parameters twice, but that doesn't really make sense, if you have the parameters at the type level you don't need them at the variant level:

enum MyEnum<T: Clone + Debug, S: Clone> {
    TupleLikeVariant(T),
    StructLikeVariant {
        content: S
    },
    NonGenericVariant(u32)
}
1 Like

Oh, I see. I thought OP was just proposing a mild generalization of "enum variants should be first-class types", I didn't realize any erasure of type parameters was involved.

(And I only meant to add type-level type parameters to the bottom half of the hypothetical isomorphism. That they got into the top half as well was an editing mistake.)