Can enum case parameter be generic?

Hi all,

I'm a new learner of rust and just come across enum chapter of the book. In my understanding, enum cases with parameter act similarly to a function, which takes some value and produce a value of enum type.

My question is: is it possible to make enum parameter a generic? some thing like:

enum Number {
  Infinite,
  RealNumber<T: std::cmp::Ord>(T),
}

fn main() {
  let num: Number = Number::RealNumber<u64>(0);
  if let Number::RealNumber<u64>(value) = num {
    print!("num is a real number: {}", value)
  }
}

The code above looked sound (at least to me) and necessary to handle object with a certain trait. However rust reference does not allow templates in enum case. Is there some thing can achieve similar which I missed?

1 Like

Enum variants are not types on their own - enum as a whole is. Generic parameters also appear only on enum as a whole, like this:

enum Number<T: std::cmp::Ord> {
  Infinite,
  RealNumber(T),
}

fn main() {
  let num: Number::<u64> = Number::RealNumber(0);
  if let Number::RealNumber(value) = num {
    print!("num is a real number: {}", value)
  }
}

Playground

4 Likes

Thanks for the suggestion. I understand that generic can apply to enum as whole, but I was talking about different use case of generic in enum. Few things I think generic on entire enum is bad in my case:

  1. Generic on enum applies to all cases. It will create multiple Number::Infinite in my example and they are not equal.
  2. Generic on enum separates generic type declaration from where they are actually used. Consider an example when I need multple enum cases with different template parameter:
enum Number<TReal: std::cmp::Ord, TDigit, /*... more ...*/> {
  RealNumber(TReal)
  DigitSequence([TDigit]),
  //... more ...
}

Plus, I was not suggesting enum cases are individual types. As I mentioned, they are more like function to me, which, should also support generic.

This won't work in practice because the generic type parameter will affect the entire enum, regardless of whether that variant uses it or not.

For example, let's consider Option<T>.

enum Option<T> {
  Some(T),
  None,
}

Obviously the None variant doesn't care about T, but if we wanted to store it in a variable, how much space should the compiler set aside?

We would be storing it in a variable of type Option<T>, but the actual size of Option<T> will change depending on whether T is a u32 or a String or whatever.

Therefore, even though the None variant doesn't directly reference the T type parameter, it still needs to be apply to the overall Option and not just the Some variant.

Enum variants aren't functions, they are part of a type definition.

The compiler might generate helper "methods" with the same name so you can use syntax like Number::RealNumber(x), but that is just syntactic sugar for constructing an instance of Number with the RealNumber variant.

2 Likes

Thanks Michael-F-Bryan for the answer.

Would monomorphization mentioned in rust book help here? Given invocations of enum case are known at compile time, complier should be able to expand from

enum Number {
  RealNumber<T>(T),
}

to

enum Number {
  RealNumber_u32(u32),
  RealNumber_u64(u64),
  ...
}

Which has a fixed size.

Compiler could still generate something with generic syntax support, when type size is not a issue.

What happens when your code uses both a Number<f32> and a Number<[u8; 1024]>?

If the compiler tried to combine all RealNumber variants into a single enum then each Number<f32> will take up 1k of memory instead of 8 bytes (4 bytes for the float, 1 byte to tell which variant is in use, and 3 bytes of padding to keep things aligned in memory). The compiler could implement it that way if it wanted to, but there would be no benefit.

Another important part of Rust is being able to predict the behaviour of your progam and having good control over your memory. I would be quite annoyed if my function's cheaply copyable Number<f32> parameter was silently turned into an expensive copy because some piece of code on the other side of the codebase decided to use a 1024-byte array as the number type (I dunno, maybe they're implementing big integers or doing crypto).

1 Like
enum Number {
  RealNumber<T>(T),
}
enum Number {
  RealNumber_u32(u32),
  RealNumber_u64(u64),
  ...
}

Ignoring that variants are not types for a moment: that's not monomorphization, that's something more like variadic generics. (It'd probably be a RealNumber<(u32, u64, ..)> or something.) Rust doesn't have variadic generics yet. A type parameter can only correspond to a single concrete type.

Back to mostly-present-day, where variants are not types and there is no variadic generics. If the generic could be on the variant, what's the type of this variable?

let n /* : ? */ = Number::RealNumber::<f32>(0.0);

It would have to be a Number<RealNumber=(f32,)> or something. But unlike an associated type, the f32 not the output of an implementation, it's still an input to the type. You're really still generic over the enum. You may have moved the type parameter closer to the variant, but you've also moved it farther away from the actual type. (Also note that multiple variants could utilize the same type paramter.)


Let's look at construction helpers for a minute. Tuple types and unit structs have construction helpers:

struct Unit;
struct Tuple(i32);
struct GenTuple<T>(T, f32);
struct S { f: f64 }

fn main() {
    // Can't do this due to the `const`-like `Unit` construction helper
    // let Unit = String::new();
    
    // Can't do these due to the `fn`-like tuple construction helpers
    // let Tuple = String::new();
    // let GenTuple = String::new();
    
    // This is fine because there is no other-item-like `S` helper
    let S = String::new();
}

The flip side is that you can't do let x = S; or let x = S(f64), because S has no construction helper. You have to use "literal struct syntax" like

let x = S { f: 0.0 };

And although they're not types, enum unit variants and tuple variants also have construction helpers:


enum E<T> {
    Unit,
    Tuple(i32),
    GenTuple(T, f32),
    S { f: f64 },
}

fn main() {
    let u: E<String> = E::Unit;
    let gt = E::GenTuple(vec![0], 3.14);
    // You can even get a function pointer to the construction helper
    // Note that it returns the enum type (because variants aren't types)
    let fp: fn(i32) -> E<()> = E::Tuple;
}

But just like before, if you're not a unit or a tuple variant, there is no helper:

    // Expected value, found struct variant
    // let s: E<String> = E::S;
    
    // Expected function, ... found struct variant
    // let s: E<String> = E::S(0.0);

    // Works
    let s: E<String> = E::S { f: 0.0 };

Hopefully this highlights that variants aren't functions. It's just that tuple variants have a function-like helper. Can the helper be generic? I'd probably say technically no for variants, it's the enum that is generic... but from a practical perspective, they might as well be. Such as in the example above:

    // The compiler inferred that `gt: E<Vec<i32>>`
    let gt = E::GenTuple(vec![0], 3.14);
1 Like

For the record, this feature is known as generalized algebraic data types (GADTs). Some languages such as Haskell do support GADTs, but Rust (at least currently) does not.

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.