-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#[derive(Default)]
on enum variants with fields
#3683
base: master
Are you sure you want to change the base?
#[derive(Default)]
on enum variants with fields
#3683
Conversation
Support the following: ```rust enum Foo { #[default] Bar { x: Option<i32>, y: Option<i32>, }, Baz, } ```
960b1f0
to
1304d15
Compare
Under drawbacks, I'd argue that this actually reduces complexity of the standard library. People know about |
@Lokathor I'm unsure if I'm reading you right, but that sounds like a positive to me 😄 |
Yes, exactly my point. The RFC text says this is a drawback, to have increased complexity. That might be true from a compiler internals perspective, or whatever. However, from an end user perspective, things that "just work" without needing exceptions listed for when they won't work in rare cases, those things are less complex. So, in other words, I'd delete that part of the Drawbacks section. This doesn't increase complexity from a user's perspective. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the reason this was excluded in the previous RFC that added #[default]
is that its interactions with generics aren't obvious - this RFC should likely be updated to address that.
@jplatte that's listed in the Drawbacks section i think, could you be more specific about what you expect the RFC to say about generics? |
I was wondering what the generated bounds would be on a generic enum that uses this feature. Currently, Let's consider this #[derive(Default)]
enum MyEnum<T, U> {
#[default]
Foo {
field: Option<T>,
}
Bar(U),
} I guess there wouldn't be any bounds on |
There would have to be a bound on It's possible that additional bounds might end up in the mix, because we don't always have perfect derives, as mentioned in the Drawbacks section. For example, the "current behavior" could arguably only apply to unit variants of enums being the default, and field-having variants would (accidentally) drag in all the generics. |
I don't think there would have to be. The more "obvious" non-perfect-derive expansion would be |
Well, Or to be more general, if we have a field type using a generic,
And the Proc Macro doesn't know which case it is when it generating the impl. So, just generating a bound based on just |
(emph mine) I know all about this, but the "we only care" statement does not apply to any derives in std currently. That's why I think this RFC should make an explicit choice as to whether the Might also be worth discussing potential interaction with private enum fields (not sure whether there's an RFC), as IIRC one of the things perfect derive has been criticized for is that it adds more cases of internal changes (adding a private field to a struct) possibly leaking out into public interfaces (bounds). |
One thing I realized while I was out is that #3681 would allow us to side-step the lack of perfect derive. The only way of doing perfect derive is with type system access. The other two alternatives are to add a way to specify bounds with an attribute, or force the #[derive(Default)]
struct S<T> {
a: Option<T> = None,
} and the expansion would need no bounds on |
How so? You could generate
Unfortunately this doesn't seem true to me. I would actually argue that #3681 makes things more complicated: Imagine if |
The only issue that I could think of when doing that blindly is the case of a recursive type, like the following case: #[derive(Clone)]
struct A<T> {
x: Option<Box<A<T>>,
} Currently the expansion in that case is impl<T: Clone> Clone for A<T> {
fn clone(&self) -> A<T> {
A {
self.x.clone(),
}
}
} The naïve expansion of this with an attempt at perfect derives could be impl<T> Clone for A<T> where A<T>: Clone {
fn clone(&self) -> A<T> {
A {
self.x.clone(),
}
}
} which would be accepted by the compiler, but never allow anyone to call
FWIW, that still requires |
I'm not sure that the fact that the derive might do a weird thing in a small fraction of cases is even particularly a problem, because people are never required to use the derive for Default. Slightly separately i also think less cases would be weird with Default than with the Clone example you give. |
@ehuss I just noticed that you tagged this T-lang, but isn't this more like a T-libs-api concern? |
Attributes are handled by the lang team. I added t-libs-api (similar to #3107). In general, teams should feel welcome to add themselves or other teams (and remove themselves if they want). I also expect teams to exercise good judgement in case other teams should be involved. |
After a quick conversation with niko, he mentioned that the general version of perfect derives is blocked on resolving the cycle case when it is indirect: struct A<T> {
x: Option<Box<B<T>>,
}
struct B<T> {
x: Option<Box<A<T>>,
}
impl<T> Clone for A<T> where B<T>: Clone {
fn clone(&self) -> A<T> {
A {
self.x.clone(),
}
}
}
impl<T> Clone for B<T> where A<T>: Clone {
fn clone(&self) -> B<T> {
B {
self.x.clone(),
}
}
} This case is not detectable purely from the token tree/AST. @compiler-errors you're looking at the trait resolution side of this, right? |
@estebank: Not sure why I got a ping directly lol, but this is blocked on coinduction which is blocked on the new trait solver. |
As mentioned by @Lokathor above, reducing the number of special-cases is generally a win with regard to complexity (for the end-user). Following on this, I would suggest moving on with the imperfect derive, as a struct would. Or any other derive than #[derive(Default)]
enum MyEnum<T, U> {
#[default]
Foo {
field: Option<T>,
}
Bar(U),
} to impl<T: Default, U: Default> Default for MyEnum<T, U> {
fn default() -> Self {
Self::Foo { field: Default::default() }
}
} It's very much imperfect:
But it's consistent, and while consistency may be the hobgoblin of little minds, it does make it easier to learn and use the language. It's simple enough to write the implementation manually in the few cases where it matters, and it's not like we all aren't used to it. A later RFC/feature can take care of handling smarter derives for all. Deriving `Clone` on enums.#[derive(Clone)]
pub enum Enum<T> {
Foo(PhantomData<T>),
} Expands to: pub enum Enum<T> { Foo(PhantomData<T>), }
#[automatically_derived]
impl<T: ::core::clone::Clone> ::core::clone::Clone for Enum<T> {
#[inline]
fn clone(&self) -> Enum<T> {
match self {
Enum::Foo(__self_0) =>
Enum::Foo(::core::clone::Clone::clone(__self_0)),
}
}
} When |
@matthieu-m It wouldn't be consistent with how |
Just for reference, and to expand on what jplatte said, when you have a unit variant as the default you get a default impl that doesn't depend on all the generics being Default, because the generics aren't fields of the unit type. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=be6e8ae45184fddb99d916ec6a237a25 So yes, a default bound on all generics regardless of where they're used is definitely too much bounding, and would not be consistent. But again, I don't think we need a bound on the generics used by the default variant, what we would need is a bound on the exact field types used in the variant. The way that the generic is wrapped in other types is important. |
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
- Should we wait until [perfect derives] are addressed first? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But again, I don't think we need a bound on the generics used by the default variant, what we would need is a bound on the exact field types used in the variant. The way that the generic is wrapped in other types is important.
i32
isDefault
, but&i32
is not, so if a field is&T
then a bound onT: Default
is the wrong bound.
@Lokathor but it is the same wrong bound we have for struct
s. What you want is perfect derives, which is explicitly called out in the text: do we wait for perfect derives or land support for the same imperfect behavior we have today for struct
s.
The reason we can't do Option<T>: Default
is because we only have AST information on these macros, and the only way to provide those bounds correctly is to detect cycles, both direct (we can detect those) and indirect (we can't detect those unless we have resolve/typeck info).
As I'm writing this though, I'm thinking that it could be that we could change the desugaring of derive
to use the more accurate where
clauses, and have a late lint check for cycles after the fact and tell people to instead write an explicit impl
instead 🤔
But that would be its own RFC for "get closer to perfect derives without trait solver changes", and I'm gonna be off-line for several weeks, so I can't drive that now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we wait for perfect derives or land support for the same imperfect behavior we have today for structs.
If we can have it be better than before, I don't see a reason to not make it better than before. If it means that enum
can (temporarily) derive more precisely than struct
that seems completely fine to me.
I mean we could start with a bound on all the field types and also on the generics (that's what structs do now), but we literally can't have a bound just on the generics, without the bound on the field types. That actually won't work, which is my point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty confused, what do you mean when you say we can't do it / it doesn't work? It is what we have for structs now, why couldn't we do the same thing here? Yes, it's not going to always generate an implementation that passes typecheck. But the same is true for structs today, and AFAIK this is not considered a major problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is basically what i mean, yes. That the code will have a build error.
It's another possibility yes. It's inconsistent with the derivation of every other trait, but no other trait has a |
@estebank the enum in the PR description has no |
That's true, but it's not how our derives work, and this was discussed above. I would find it quite surprising if we had two fundamentally different ways to generate code for derives of "things with fields". If we have a way to generate better derives (adding bounds on the fields, not the generic types), we should do that consistently. The "enum variant without field" case is fairly harmless, but once there are arbitrary fields this is just going to look like more special cases, that work better in some situations but worse in others. |
Note that we have a discussion right now in #3681 about whether this is even possible. Also, I think people are assuming that only the fields in the default branch would generate a default bound, but existing derive macros have no mechanism for being that specific. That is:
You guessed wrong. It wouldn't be either of those, it would be I think this distinction should be made clear in the RFC, because I think a lot of people would make the same assumption as the one in the quote. |
Support the following:
Rendered