Allow deriving fields as struct inside enum itself (enum variants as types)

Hey! I'm not sure either if this feature request title reflecting on what I want or if it was proposed before. Let's assume we have an Enum but one field is referencing another dependent field itself. i.e.:

OK

pub enum Object {
	Foo(Vec<String>),
	Bar {
		qux: String,
		quux: String,
	},
}

COMPILE ERROR

pub enum Object {
	Foo(Vec<String>),
	Bar {
		qux: String,
		quux: String,
	},
	Bars(Vec<Object::Bar>) // not a type
}

Playground

I think what you meant to search for is "enum variants as types"

3 Likes

See Enum variant types by varkor · Pull Request #2593 · rust-lang/rfcs · GitHub for some current discussion about this

2 Likes

Up until now, things that are considered values are unusable at the type level. Const generics is excepted from that, but that feature is quite restricted, and only deals with values of the builtin types as of now; enums and structs are unsupported atm.

If types and values are conflated, wouldn't that cause at the very least weird quirks in the type system as well as conceptual issues? I mean how am I supposed to reason about something that's considered both type and value?

I think you are slightly confused as to what the RFC proposes. All it proposes is that every enum variants is also a type in its own right.

As for both types and values, how is this different than structs with all pub fields? You can pattern match with it (so it is a type) but you can also construct one (so it is a value).

This isn't a way to use variables as some form of dynamic type or pattern matching construct.

1 Like

This is precisely what I think the RFC proposes. And I'm concerned about the consequences of implementing that. Say this RFC is accepted. Then:

  1. If I implement a trait for an enum variant, how does that relate to the enum as a whole? If such a trait implementation is not allowed, then it breaks the mental model of what can be done with a type.

  2. How about if I implement a trait for the enum as a whole? Is the trait then also implemented for any or all of the variants?

  3. If I define an enum, can I implement methods for only certain variants? If not, then that breaks the mental model of what I can do with a type. But if so, how is Rust to resolve which methods I can call on a value of the containing enum type? If I have to use an if let or match in order to destructure first, then the "enum variants as types" isn't really pulling its weight IMO.

These are just the questions I could think of atm that would need to be resolved first.

Those are good questions for the RFC PR itself

Just some quick answers, more discussion should be in the RFC thread.

It's just implemented for the variant, not the enum as a whole.

This one you could definitely make an argument that it should apply. A variant type is a direct subset type of the full enum type. I think the "best" solution would be to provide a default impl, so the variant type could also provide a specialized impl? Or maybe monomorphized optimization could just make that specialized impl for you?

The default position if this concern isn't raised would be that no trait implementations would be implied, as they are separate types. However, IIRC, enum variant types coerce to the enum type, so (something something inference) it might "just work".

Almost certainly.

By refining the type to the type which implements the method. They're different types, after all!

I assume your point here is that what benefit you get over using newtype variants around actual normal distinct types. It's mostly convenience, but one actual additional power you get is to use &Enum::Variant where &Enum is expected (if the RFC allows that; the representation they argue for does allow for it to exist). (Namely, Enum::Variant still carries the discriminant and padding with it, where a full wrapped EnumVariant type doesn't and would need to be rewrapped to be used as an Enum.)

1 Like

If I implement a trait for an enum variant, how does that relate to the enum as a whole?

From the RFC:

Variant types may not have inherent impls, or implemented traits. That means impl Enum::Variant and impl Trait for Enum::Variant are forbidden. This dissuades inclinations to implement abstraction using behaviour-switching on enums (for example, by simulating inheritance-based subtyping, with the enum type as the parent and each variant as children), rather than using traits as is natural in Rust.

How about if I implement a trait for the enum as a whole? Is the trait then also implemented for any or all of the variants?

Implementing a trait for the enum works the same as it usually does, given that variants cannot have specific implementations.

There has been discussion about allowing impls on variants, but the RFC is intentionally conservative and leaves this as a future extension.

3 Likes

I would expect enum variants as types to mean that

enum Foo {
    Bar,
    Baz,
}

we now have 3 types: Foo, Bar and Baz kind of like

struct Bar;
struct Baz;

enum Foo {
    Bar(Bar),
    Baz(Baz),
}

where Bar and Baz coerce to Foo

fn takes_foo(f: Foo) {}

takes_foo(Foo::Baz);
takes_foo(Baz);

and thus

impl Trait for Foo {
    fn do_stuff(self) {}
}

// we can now call `do_stuff` on Baz because
(Baz as Foo).do_stuff();
// and thanks to implicit coercion 
Baz.do_stuff();

// however
impl Trait for Baz {
    fn do_stuff(self) {
        println!("Hello from Baz!");
    }
}

Baz.do_stuff(); // prints "Hello from Baz!"
Foo.do_stuff(); // does nothing as expected
2 Likes

It can't be that simple, because the following code would break:

enum Foo1 { Bar }
enum Foo2 { Bar }

The types would need to be namespaced with their enums somehow and not freestanding, unless there is some other way to avoid the conflict.

2 Likes

Perhaps Foo1::Bar could be made to behave as either a type or a variant constructor, depending on context which it's used in. Alternatively, Foo1::type::Bar syntax is available.

1 Like

Currently, given mod foo { pub struct Bar; }, you can use foo::Bar either as a type or a value constructor, depending on context. Is there some known ambiguity which would prevent doing the same with a type nested in another type rather than a module? (Looks like you can't use type aliases (nested or not) as constructors in the current language, which is honestly rather surprising!)

2 Likes

It's quite possible that the module example you gave causes exactly that kind of ambiguity, specifically in the :: resolution operator. But I'm not entirely sure.