Inconsistency in how variants of generic enums can be created

Hello everyone.

Having an enum like this:

enum Foo<'a> {
    A(&'a u8),
    B(Cow<'a, u8>),
    C(u8),
    D { x: u8 },
}

I can create variants of the enum using the syntax Foo::<'a>::X ..., where "X" is "A", "B" or "C", but not "D", for which I have to write just Foo::D ...

E.g.

fn foo<'a>(x: &'a u8) {
    let val = Foo::<'a>::A(&x);
    let val = Foo::<'a>::B(Cow::Borrowed(x));
    let val = Foo::<'a>::C(*x);

    // let val = Foo::<'a>::D { x: *x }; // Error: lifetime arguments are not allowed on variant `D`
    let val = Foo::D { x: *x }; // This compiles.
}

(Btw it doesn't matter that D doesn't use 'a, only the fact that it's a struct-like variant matters).

The question is: why is it so? Is it a bug in the compiler that should be reported?

To be clear, I've hit upon this issue when using a 3rd-party derive macro, which produces these Foo::<'a>::X calls. I don't think the macro itself can get rid of the turbofish, because it has to support all kinds of generic parameters on enums.
So the only solution for me is to refactor the enum itself, so that it doesn't use struct-like variants, which is a bit annoying.

1 Like

I believe the first 3 work only because tuple variants also define an associated function with the same name, and the call with the turbofish is allowed to fallback to that function.

3 Likes

these are the constructors of the tuple struct (or tuple variant in this case), quoting the documentation:

In addition to defining a type, it also defines a constructor of the same name in the value namespace.

as already said, the syntax worked for the first 3 cases is a side effect of the tuple constructors. the correct syntax to use the turbofish operator is to put the generic parameters on the variants, not the enum itself:

let val = Foo::A::<'a>(&x);
let val = Foo::B::<'a>(Cow::Borrowed(x));
let val = Foo::C::<'a>(*x);
let val = Foo::D::<'a> { x: *x };

so whatever derive macro you are using, it is not implemented correctly.

3 Likes

Thanks!

Well, this is good news. I'll report the issue to the developer of the macro then.

But I have a follow-up question - why does it still work if the generic parameter is a type. E.g.

enum Foo2<T> {
    A(T),
    B { t: T },
}

fn foo2<T: Clone>(t: T) {
    let val = Foo2::<T>::A(t.clone());
    let val = Foo2::<T>::B { t }; // Compiles fine
}
1 Like

Arguably the turbofish-on-variant syntax is inconsistent with the rest of the language. The generic parameter is associated with the enum, not the variant (Foo::Bar::<T> would make more sense if Rust had GADTs and Foo was one, ie. if variants could actually have type parameters). The fact that Foo::<’a>::Bar doesn’t work here is clearly a bug in the grammar and should be fixed (even more obviously if Foo::<T>::Bar does work!)

4 Likes

this, I don't really know the reason. I think it could be special cased by the compiler because it's common, maybe?

yes, I agree it feels more intuitive and natural to put generic parameters on the enum type than the variants.

you can argue it was a grammar bug (there's a closed rfc2218 ), I was just pointing out how it is handled currently, which is quite the opposite: Foo::<T>::Bar worked as a sugared syntax, while Foo::Bar::<T> was the "formal" syntax.

and this is largely for historical reasons.

in the very early days (pre 1.0), rust enum variants were "flat", meaning they lived in the same module as the enum itself (similar to enum in C), e.g.:

mod foo {
    enum Bar {
        Baz,
    }
}
// it was used like this
// note the `foo::Baz`, not `foo::Bar::Baz`
let x: foo::Bar = foo::Baz;

then namespaced enum variants were added in rfc390, the enum name was used as the containing namespace for the variants, i.e.:

// this definition:
enum Foo {
    Bar,
}
// conceptually worked like this:
type Foo = /* enum type item */;
mod Foo {
    struct Bar;
}
// with generics:
enum Foo<T> {
    Bar(T),
}
// worked like this:
type Foo<T> = /* enum type item */;
mod Foo {
    struct Bar<T>(T);
}

this, however, had some unintuitive behavior, since the enum name must be used for resolution of the variants, but the variant types were NOT associated items of the enum type itself. and this was one of the motivating example of rfc2338, e.g.:

enum Foo {
    Bar
}
type Baz = Foo;
let x: Baz = Foo::Bar; // good
let x: Baz = Baz::Bar; // didn't compile before rfc2338

back to OP, you can actually use the "intuitive" turbofish syntax for lifetimes with a type alias:

enum Foo<'a> {
    A(&'a u8),
    B(Cow<'a, u8>),
    C(u8),
    D { x: u8 },
}
fn foo<'a>(x: &'a u8) {
    type Bar<'a> = Foo<'a>;
    let val = Bar::<'a>::D { x: *x };
}

but I don't know if or how this would fit the "third-party derive macro" in question.

1 Like