Request: Default non-unit variants

This code:

fn main() {}

#[derive(Clone, Debug, Default)]
enum DefaultingEnum {
    #[default]
    FirstVar(FirstVariant),
}

#[derive(Clone, Debug, Default)]
struct FirstVariant {
    first_field: u32,
}

...throws this error:

error: the `#[default]` attribute may only be used on unit enum variants
 --> src/main.rs:6:5
  |
6 |     FirstVar(FirstVariant),
  |     ^^^^^^^^
  |
  = help: consider a manual implementation of `Default`

My request is for someone to enable Default derive attribute on DefaultingEnum to use the default value of FirstVariant inside the FirstVar variant instead of saying that it can't be done.

It was included in the original RFC 3107, but there's problems around generated bound (and cyclic dependency). You can check the link for more detail.

4 Likes

Yeah, it was requested that it be removed due to the lack of consensus on what the generated bounds be. If that can be resolved, there is zero reason it couldn't be added.

would it be possible to add Default non-unit variants for one category of scenarios at a time? For example you might start by allowing Default non-unit variants in the case where the non-unit variant's inner value has a primitive type as its default (i.e. explicitly written out value, which shouldn't cause cyclic dependency), or if it's a tree of defaults then allowing the non-unit default at the root if all the leaves of the tree are primitive types.

#[derive] is a macro (even if built-in), so it is incapable of knowing the type of a field beyond what is syntactically present.

2 Likes

Why can't the compiler expand the macro to perform compile-time analysis of the expanded code?

Macro expansion operates on the AST, before types are resolved. It's essentially just a fancy form of find and replace.

1 Like

so you can then expand the macro before the type is resolved, and then you can resolve the type and perform the compile-time analysis to let the Default macro know that the enum variant's inner value is a primitive type?

I think a next useful step might be to allow it for non-generic enums?

Not just that – seems like we could allow #[default] on any variant that does not refer to any generic parameter of the enum, without having to resolve any questions about how to decide bounds.

2 Likes

I was about to say not quite — it could be a valid choice to emit the "imperfect" bounds that are generated for generic struct when deriving for generic enum — but checking, we do already allow #[default] for unit variants of generic enum without creating any additional impl bounds.

Due to that, I agree that extending #[default] to support non-unit variants that do not use any generic captures is straightforward. (Unfortunately, this includes not using generic lifetimes (or the semi-implicit capture that is the Self type), since impls can be lifetime-dependent, which could still be surprising.)

I think the only option ruled out is to emit "imperfect" bounds whenever #[default] is used on a non-unit variant, whether it captures generics or not. Given we don't emit bounds when on unit variants, I think this is a bad "spooky action" option that we should be okay with ruling out.

1 Like

I don't think that's possible for a proc macro to detect. The variant can use an associated type -- perhaps even via TAIT tricks somehow -- without naming the type parameter's token, I think.

Really? It does sound like the general [category of hazard of interacting features] that one needs to consider, but how would the compiler ever know you meant an associated type of that type parameter, rather than of some other type (parameter or otherwise) that met the same bounds, without you writing the token?

Well, the first way that comes to mind is something like

struct Foo<T> where Self: HasAssoc { x: Option<Box<<Self as HasAssoc>::Ty>> }
trait HasAssoc {
    type Ty;
}
impl<T> HasAssoc for Foo<T> {
    type Ty = T;
}

where the x field uses T without every saying T.

Maybe it would be sufficient to also block Self, but now things are getting complicated again.

Impressively terrible code, I love it. Of course, the existing derive macros can't handle it either.

Also just learned that we have an explicit exception for

error: `derive` cannot be used on items with type macros
  --> src/lib.rs:31:16
   |
31 | struct Baz<T>(T!());
   |               ^^^^
3 Likes

Note that Scott is mentioning TAIT here — type alias impl trait. You can't easily have TAIT implicitly capture generics like RPIT does, but it might be possible behind some tricks. Forbidding any mention of the generic names (including Self) before type normalization (i.e. syntactically) should in theory be sufficient.

The most obvious other potential "hole" is that despite being a semi-inert attribute, derives are expanded before a non-inert proc macro attribute which is "inside" it, seeing the attribute itself in its token stream. This means that a proc macro transformation after the derive could change the definition of the variant. However, I don't think that's an issue per say, rather just how derives work.

EDIT: whoops this post got buffered for an hour and got ninja'd

1 Like

What's the advantage of using the non-generic approach rather than generic approach?