Challenges with direct usage of enum variants in Rust structures

Hey all, I'm just starting out with Rust, and I'm strugling a bit with what the best way to define a set of types that can be used to create more complex ones.

Think of JSON, where you have your scalar types (strings, numbers, bools) which can then be used to create composite types (objects, arrays). In order for the composite types to be created, it would be useful to "group" the types in a generic type, but it would be also really useful if we could reference the individual types as well on their own.

Initially enums seemed like the way to go.

pub enum Value {
    String(String),
    Int64(i64),
    Bool(bool),
    Map(HashMap<String, Value>),
    Array(Vec<Value>),
    // ...
}

This works pretty well for the most part, but the downside is that I can't use the underlying enum types directly.

pub struct Test {
    pub v: Value,
    pub i: Value::Int64, // INVALID
           ^^^^^^^^^^^^
           |
           not a type
           help: try using the variant's enum: `crate::Value`
}

The other options I could think/find are to define either a struct or a tuple for each type.

pub struct String {
    pub value: String,
}
pub struct Int64 {
    pub value: i64,
}
pub struct Bool {
    pub value: bool,
}
// ...
pub enum Value {
    String(String),
    Int64(i64),
    Bool(bool),
    // ...
}
pub struct Test {
    pub v: Value,
    pub i: Int64, // Valid, but horrible to work with
}
// ...
let test = Test {
    v: Value::Int64(Int64 { value: 42 }), // Horrible
    i: Int64 { value: 42 }, // A bit annoying
};
// ...
let i = test.i.value; // Very annoying

This is not really fun to work with, and I'm not sure if it's even the best way to go about it.
Are there any other/better ways to go about this?

Thank you very much in advance.
Regards, geoah

1 Like

That's because enum variants aren't types.

If you want to refer to the data held by a specific enum variant with a single type, that variant will need to wrap a single type.

You wouldn't really need to add that extra layer of newtyping though, you can just refer to String in this case.

If the enum variant were a multi-field struct or tuple, then, yes, a new struct would be needed.

1 Like

Your second example is what is known as the NewType pattern, and is usually defined as a tuple struct rather than a struct with fields, for encapsulation purposes:

pub struct Int64(i64);

It's also usually accompanied by an implementation of the AsRef trait, so that you can get a reference to the underlying value:

impl AsRef<i64> for Int64 {
  //...
}

And used like this:

let my_int_64 = // Some way to construct your Int64;
let my_underlying_i64 = my_int_64.as_ref() // This is of type &i64

And, as you have discovered, it's very common to use these together with enums:

pub enum Value {
    String(String),
    Int64(Int64),
    Bool(bool),
}
1 Like

Hey @moy2010, @zacryol thank you both very much for your responses.
NewType is a term I was missing, I'll look this up a bit more.

I might be misunderstanding your comment, could you provide an example of what you are suggesting?
These types will have various methods implemented on them, so I expect they will need to have a custom typed defined.


@moy2010 thank you for the AsRef suggestion, this sounds really interesting.

Both tuple and field structs seem really odd to me (coming from go/golang) from a developer experience.
Do the following usage examples feel natural to Rust devs, or are they considered abominations?

let vi: Value = Value::Int64(Int64(42)); // (1) Having to always nest this seems odd
match vi {
    Value::Int64(Int64(i)) => { // (2) Wow, this is very explicit, didn't know it can be done
        println!("i: {:?}", i); 
    }
    Value::Int64(i) => {
        println!("i: {:?}", i.0); // (3) Having to explicitly access the inner value seems odd
        println!("i: {:?}", i.as_ref()); // (4) That's better, but still weird
    }
    _ => panic!("unexpected value"),
}

Again, thank you very much for your help.

I means that instead of

struct Str(String);
enum Value {
    String(Str),
    ///etc.
}

you could just have

enum Value {
    String(String),
    //etc.
}

You wouldn't necessarily need to to wrap a single value, the Value::String variant already has only one type in its internals.

Concatenating the construction of types like that is certainly not idiomatic. You'll more often than not see the From/Into traits being used for converting from one type into another:

let vi: Value = Int64::new(43).into().

Thank you very much both for your help. I'll go with @moy2010's into/from/asref suggestions and see where this takes me :slight_smile: