Sum types in product types

In rust you can write product types within sum types e.g.

enum MyEnum {
    V1 {a: i32, b: f64},
    V2,
}

but you can't write a sum type within a product type you would have to do this:

enum MyEnum {
    V1, V2,
}

struct MyStruct {
    x: MyEnum,
    y: f64,
}

I am wondering why this is. Could you not have some syntax for this e.g.

struct MyStruct {
    x: {V1, V2},
    y: f64,
}

You could even have product types within product types and sum types within sum types.

1 Like

What are the full names of V1 and V2 in your case? How do I use them in a match? What if I have this:

struct MyStruct {
    x: {V1, V2},
    y: {V2, V3},
}
1 Like

Are there other languages which support this?

My understanding is internally rustc represents structs as single-variant enums, so the sum-of-product-types is somewhat natural, but the opposite seems like it would need to define an anonymous type for the inner enum.

2 Likes

For fully anonymous enums I expect MyStruct is the namespace, so that would be disallowed. It could also be punted on, simply requiring the enums to have names. As long as we're discussing syntax, it might be nice for the enum keyword to still make an appearance, ala

struct MyStruct {
    x: enum {V1, V2},
    y: enum {V3, V4},
}
1 Like

I was just thinking about something like this after seeing this post regarding context in errors. Although I had the inverted idea of adding variant independent fields to enums.

enum MyEnum {
    y: f64; // present for all variants

    V1,
    V2,
}

The idea for this was to add a context to enum style errors without needing to duplicate the field into every variant.

enum Error {
   context: Option<String>;
   backtrace: Option<String>;

   Io(io::Error),
   Sql(sql::Error),
}

I know this isn't exactly a sum type in a product type. It's adding fields to enums rather than adding variants to structs, but maybe it can serve as an example use case.

I just don't know how useful these constructs would be in practice. I don't run into these situations often enough that I feel saving a few keys by not having to write a separate type is necessary.

2 Likes

It's very close to it though.

struct MyError {
   context: Option<String>,
   backtrace: Option<String>,

   error: enum { Io(io::Error), Sql(sql::Error) },
}
1 Like

Ada has this.

Instead of saying "we can treat a sum type as a generalisation of an enumerated type", it says "we can treat a pair of a product type and a sum type as a generalisation of a record type".

1 Like

C allows anonymous unions (and structs too) inside structs. Typescript has union types (spelled foo | bar | baz) that are very often used in ad hoc style inside types and in function parameters. Scala has similar union types as well. And there have been previous discussions about adding anonymous enums to Rust too – in particular in the context of error management.

1 Like

Yes, that certainly seems better.

Another thing that came to mind is that the enum itself may require a name to do things like:

// #[derive(Default)] // not allowed unless`x`doest `impl Default`
struct MyStruct {
    x: enum {V1, V2},
}

impl Default for ??? {
    fn default() -> Self {
        ???::V1
    }
}

Attributes could be added, but then we only support traits which have derive() macros available? That seems…unfortunate. This feels very adjacent to "enum variant types" (I'm unsure of the most recent status/issue) where enum MyEnum { A { x: u64 } } can be "captured" as A { x: u64 } after matching on an instance of MyEnum. Given that MyEnum::A is not a type, this doesn't work today. However, this does not affect #[derive()] for MyEnum as much because the default behaviors over a sum type are easy. With a product type, the questions have to be answered for all fields anyways (Default is the main problem here). With a nested sum type, the compiler now has to…make a choice. I suppose #[default] can work here, but I'm hesitant to say it is the only trait affected. But maybe it's fine and #[default] and other derive()-associated attributes are all that's needed. But it is unfortunate that one is limited ot derive() macros for any impl Trait for InnerEnum fields.

struct StructName {
     x: f64,
     y: enum { VariantName1(Type1), VariantName2 }
}

This seems to solve the issue of naming:

let s: StructName = …;
match s.y {
    StructName::VariantName1(a) => foo(a),
    StructName::VariantName2 => bar(),
}

EDIT: since this is possible:

struct StructName {
     x: f64,
     y: enum { VariantName1(Type1), VariantName2 },
     z: enum { VariantName1(Type3), VariantName4 },
}

This means that StructName::VariantName1 can only be used in destructuring context (like in match or if let clause) where it is clear of which VariantName1 we are talking about, and it would not be possible to use it in function parameters, return type or explicit variable type.

match s {
    {x: V1, .. } => <some code>,
    {x: V2, y: V3 } => <some code>,
    _ => <some code>
}

maybe?

Sure, i was really suggesting the idea of doing this, the i don't have strong opinions on the syntax.

Personally I would consider this an anti-feature for the simple reason that it doesn't solve anything, or make anything new possible, yet still wants to spend the really scarce complexity budget that the language has left to spend.

If this complexity budget threshold is exceeded, we end up with C++: a language so gargantuan that Cthulhu looks like a Chihuahua in comparison. A language that cannot reasonably be used fully within a project, or fully understood by 1 person, with the undesirable side effect that projects have to define allowed dialects of the language, splintering knowledge and the ecosystem in the process.

No single change will make that happen, but changes like this would surely push Rust closer to that edge, with nothing useful in return.

3 Likes

Ok then, get rid of product types within sum types to make the language more simple.

That would be more consistent.

The issue with that is that it's a stable feature, and as such will be supported until the End Of Time™️.

So, in principle, you are against product types within sum types? Yet it is part of the language, so obviously the language goal and your principles do not match.

I haven't said anything about how I feel about this feature, please don't put words in my mouth.

What I said is a matter-of-fact statement: Rust has strong stability guarantees, and they apply to every stable feature.

4 Likes

You need some way to make name the variant anyways, so it may as well be a separate type. No such requirement applies to product types.

There are downsides and upsides to both. Layout optimizations are an obvious upside, but people also get annoyed that you can't use enum variants as types, which forces them to split out the products into separate types anyways.

I've given a way to make the variants in my original post. As have some others as replies.

And the "people getting annoyed that you can't use enum variants as types" would also apply to the product types within sum types scenario, with people not being able to use the inner product type.

If we allow to write

struct MyStruct {
    x: {V1, V2},
    y: f64,
}

let ms : MyStruct = MyStruct { x: V1, y : 2.0 };
let msx  : ??? = ms.x;

then what is a type of ms.x? It is deferentially not a MyStruct. But what?