-
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
Default Struct Field Values #1806
Conversation
Interesting, I like it. Not something that really bothered me so far, but I can see the appeal. You mention "constant expressions" a few times. Does mean you want to allow For alternatives, I see there is derive-new crate (using macros 1.1 which is currently unstable), that has an open issue for adding default values with attributes (I assume something like adding |
@killercup |
@leodasvacas We could make |
The goal is to maintain that expectation of cheapness for fields that can be initialised without you knowing. We were talking about allowing |
I have a problem with this not playing nice with the trait machinery, if you have a function that takes a generic #[derive(Default)]
struct Foo {
a: i32,
b: i32,
}
Foo {
a: 10,
..Default::default(),
} is slightly unergonomic, but it's explicit and allows a user wondering where values come from to follow the initialize_b_only({
a: 10
}) (syntax is imaginary). |
This comment has been minimized.
This comment has been minimized.
@jFransham I think this plays just as nicely with traits as Maybe this is a different case because you're actively giving each field a default value and the result is that it can be initialised with no outside data. So it basically is I think an important distinction between what we've got now and what this RFC proposes is that it lets structs define their own default for types on a per-field basis. So instead of having one value for Having less boilerplate around builders would definitely make them more useful here, but are still more effort. |
This comment has been minimized.
This comment has been minimized.
text/0000-default-fields.md
Outdated
} | ||
``` | ||
|
||
We can add a new field `b` to this `struct` with a default value, and the calling code doesn't change: |
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.
Unless ..
is made optional in patterns new field b
can't be added here because it would break patterns Foo { a, b }
.
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.
However, if structure has a special private field to force ..
in patterns - Foo { a, b, .. /*__hidden: ()*/ }
, then it can be extended.
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.
My immediate thought is that ..
in patterns would also have to be optional then for consistency. I'm cautious about that though because it might increase the scope of this thing too much.
The other option is make ..
required for structs with defaults, or stop pretending it's always backwards compatible.
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.
It is unfortunate for partial construction and partial destructoring to have an inconsistent syntax. If making ..
optional in patterns is an acceptable way forward, then I would suggest for this RFC to mention that because of patterns you need a private field for backwards compatibility, and leave changing pattern syntax to a future RFC.
text/0000-default-fields.md
Outdated
|
||
## Explicit syntax for opting into field defaults | ||
|
||
Field defaults could require callers to use an opt-in syntax like `..`. This would make it clearer to callers that additional code could be run on struct initialisation, weakening arguments against more powerful default expressions. However it would prevent field default from being used to maintain backwards compatibility, and reduce overall ergonomics. If it's unclear whether or not a particular caller will use a default field value then its addition can't be treated as a non-breaking change. |
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.
However it would prevent field default from being used to maintain backwards compatibility
There's no backward compatibility already (https://github.com/rust-lang/rfcs/pull/1806/files#r90957633), so mirroring pattern syntax or omitting ..
is mostly a stylistic choice (unless, again, ..
is made optional in patterns).
This comment has been minimized.
This comment has been minimized.
Not sure how I feel about this RFC, but syntactically I would strongly prefer some indication of the missing fields in the constructor, possibly just That is: struct Foo {
bar: u32 = 0,
baz: u32 = 1,
}
let foo = Foo { bar: 7, .. };
let foo = Foo { .. }; |
@withoutboats I'm not against requiring |
I support adding Right now, X is usually an implementation of |
Well that would also save having to change the destructuring pattern so ok I'm sold. |
Yea, the goal would be to make it clear to people what is happening in the code they're reading, since they aren't necessarily familiar with the definition of the structs involved. I think this is a case where "explicit is better than implicit" is a helpful maxim. |
One thing that I haven't yet seen brought up is the interaction with (potential, admittedly) function argument defaults, along with defaults in other places. Do we want to settle on |
@Mark-Simulacrum |
Now something to figure out is how to explain field defaults with |
Requiring |
I would rather like to see work on named and optional arguments, which would allow constructors to solve this problem, and make other APIs more ergonomic as well. If Rust had to pick between optional fields for structs, and optional/named function arguments, I would pick the latter, as it allows more ergonomic APIs, e.g. |
@crumblingstatue I don't see why Rust couldn't have both default struct fields and default function arguments. |
As @KodrAus already mentioned, making this explicit with @crumblingstatue This is a much smaller adition to the language than named or optional parameters and yet solves some use cases that would need both of those features, so it's a win for those who want such features even if it dosen't solve all use cases. Edit: Change of wording. |
@leodasvacas I'm thinking a pragmatic way forward would be to propose this with an explicit syntax so it's in line with the current state of the world. The biggest benefit of implicit syntax kind of falls over with patterns right now, so they seem like a coupled issue. If callers want expliciteness it's going to come at the cost of incompatibility because everything is surfaced to them. A future RFC could propose allowing implicit The similarity but difference to FRU is the problem I'm thinking about at the moment. |
I oppose this RFC, for the following reasons:
Overall, this RFC, while seems somewhat useful, doesn't seem to provide enough and strong enough advantages to warrant a language change, while it is also problematic (in terms of being future-proof) in the face of structs with private fields. I don't find the motivation very convincing either. I understand that the proposed syntax is somewhat more convenient to use than writing a builder pattern or more than one constructor, but these mainly (with one exception) concern the person implementing an API, not those consuming it. The exception is of course the "config struct in constructor parameter" pattern, |
This makes the
It couldn't because making it a library would be a breaking change; at most it can be a sort of quasi library proc macro that the compiler provides by default. In any case, if we implement this RFC, nothing changes. It can still be a library and the crate
"Magic" is in the eye of the beholder. When pattern matching a tuple with I also feel that there's a sort of duality between pattern matching and ignoring the extra fields and constructing and providing extra fields. I don't think the usual "pattern matching follows construction" is violated here. There are even nice laws here: let x = Foo { alpha, beta, .. };
let Foo { alpha, beta, .. } = x;
let y = Foo { alpha, beta, .. };
assert_eq!(x, y);
I think there is something mostly "unimportant" and "ignorable" going on, otherwise I wouldn't use default values on fields. I also don't think I think that if defaults were being used when writing
Sure, sometimes they do; however, even with pub struct Unique<T: ?Sized> {
pointer: NonZero<*const T>,
_marker: PhantomData<T> = PhantomData,
}
Unique { .. } // Error! `pointer` is missing.
Unique { pointer: <value>, .. } // Still, error! `pointer` isn't visible...
It is always possible to add a private field typed at some unit type; now the type cannot be constructed with
I think the language (+ team) should be concerned with making libraries (and libraries within applications) convenient to write. |
Of course not now – but it could have been, that's what I meant.
I see that, but that's mostly ignoring the semantics. (Also just because it's mathematically appealing, it can have issues in practice, which I think this construct definitely has, as I mentioned before.)
Sorry, then I think we are just disagreeing here.
So could this RFC with two of the proposed semantics. Only one of them prevents side effects; the others (allowing arbitrary expressions or only
Yes (but I doubt many users would know this or do it by default out of caution…), and then this part of the RFC completely loses its use anyway. By the way, this would now be yet another thing that programmers implementing an API would need to remember – "always add a mostly meaningless ZST private field by default, if you want to prevent construction via a literal". I think this is the wrong way around, as it makes the potentially dangerous situation the default, requiring one to opt out of it (and not even in a way that might be obvious to people).
Definitely. I still don't think this particular RFC is justified. It's not like structs are hard or inconvenient to use today. |
But what value does the argument have if it's not actionable?
Yeah sure, just because there are mathematical laws doesn't mean it cannot have other problems, but then those need to be demonstrated (and I don't think sufficient problems have been...).
I think we should discuss what the RFC actually proposes, and it proposes that the default value of a
I disagree that what you need to remember is "always add a mostly meaningless ZST"... I think almost always the other fields that don't have defaults are private (and then you wouldn't The language is set up in such a way that the default thing is to respect the principle of least privilege since fields are private by default. If your API is dangerous in some way such as if it involves struct Id<A, B> {
prf: PhantomData<(fn(A) -> A, fn(B) -> B)> = PhantomData,
}
impl<S: ?Sized, T: ?Sized> Id<S, T> {
pub fn cast(self, value: S) -> T where S: Sized, T: Sized {
unsafe {
let cast_value = mem::transmute_copy(&value);
mem::forget(value);
cast_value
}
}
}
let refl: Id<u8, Box<u8>> = Id { .. }; // And we have bricked the type system. but in this case you used |
The point was that
This is exactly what I'm trying to say (assuming by "it" you are also referring to |
Right, I think that's a worthwhile goal. But do you agree that this RFC doesn't change anything in this respect -- so the principle is retained? With respect to how the principle is even enhanced, consider for example #[derive(Debug, Arbitrary, Deserialize)]
struct Foo {
// The proptest_derive macro will see this
// and fix the value of `bar` to always be 42.
// Serde will see this and assume that if no value is
// provided then `42` will be used on deserialization.
bar: u8 = 42,
// other fields...
} |
I was asking if it does - and if it doesn't, I do agree it is retained. |
I'll tentatively disagree with the constructor If I understand, the I'd agree with @H2CO3 opposition to this feature if I also think
And this RFC's ergonomics can be recaptured with polymorphic constants:
We could then simple write |
My main problem with this feature is that it's a less generic solution than what could be achieved with constructor functions if Rust had named and optional args. Quoting from the motivation:
This problem would be nullified if Rust had optional and named arguments. Reasons why it's less generic than constructor functions
I don't fundamentally oppose this feature, but I believe optional and default arguments would cover all the motivation for this feature, without the constant-only drawback, while also fulfilling other motivations. As a closing statement, I want to acknowledge that optional/default arguments are indeed controversial, and they may take years to land, if ever. I just wanted to make sure this information is out there for people to consider. |
I believe structural records #2584 along with this feature provide precisely named and optional args since you could write:
|
I don't mean "dual" in the literal category theoretical sense; but it does give such an "aura". This is sort of vague, but I hope you understand kinda what I mean. As for using
This is true because of
I think code writers benefit from this as well; it gives me great comfort when writing code to know that side effects are not being done behind my back.
Possibly, but the motivation isn't solely as an alternative to named and optional arguments (which are quite controversial on their own because of a host of problems particularly with the former...).
But you would get other problems, for example, some variants on named arguments make function names part of public APIs and most variants of named arguments don't interact well with the trait system. Most floated solutions to optional arguments don't mesh well with how defaults work elsewhere in the language.
The drawbacks are outdated. T-libs could make
Yes, however, optional and named arguments does not enhance
A constructor function can make use of the default values and not make use of others... You can still check the invariants that need to be checked if there are any. I don't think default values need to solve all problems. ;)
I have demonstrated, in this, and other comments, that optional + named arguments do not cover all the motivations.
Very fair! :) |
Making sure these two features work together to allow this use case is definitely worth looking into. It would not cover all the use cases of optional and named args ( Uhh, never mind the part about runtime parameters. Those only apply to the implicit defaults, the user could still explicitly provide runtime-dependent values. Actually, I really like this idea. Even for those use cases that default struct field values don't cover, you could write your constructor function using structural records and default values combined. If these features end up working together, they might be the answer to optional and default args. |
I'm endorsing the feature in what I wrote @Centril : You'd want a Also, easily readable code aids in correctness, while ergonomics favorable to writing code without being favorable to reading code tends to harm correctness. I'm mostly saying |
I view this feature as a foundational step towards named and optional arguments. Named arguments should have struct-like syntax and desugar into structs of some kind. With such a feature, then default fields gives you optional arguments 'for free'. |
This seems like a pretty useful feature, but I don't understand the rationale for allowing the construction of structs that contain invisible fields with it, especially given that FRU isn't supported for structs with invisible fields. |
@jplatte Maybe this feature could be combined with a non-exhaustive flag, so #[non_exhaustive]
struct Options {
first: bool,
} would not allow instantiating the struct, similar to if you did struct Options {
first: bool,
#[doc(hidden)]
__private: ()
} and #[non_exhaustive]
enum Example {
Variant1,
Variant2,
} is equivalent to enum Example {
Variant1,
Variant2,
#[doc(hidden)]
__NonExhaustive,
} This has been discussed before I think - there was talk of syntax like enum Example {
Variant1,
..
} but I can't remember the details of the conversation. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
A lighter-weight alternative to this RFC is:
Both are non-breaking changes, and would be sufficient to allow this: #[derive(Default)]
struct Person {
name: String = "Jane Doe",
age: u32 = 72,
occupation: String,
}
fn main() {
let person = Person::default();
assert_eq!(person.name, "Jane Doe");
assert_eq!(person.age, 72);
assert_eq!(person.occupation, "");
} There are still issues here, but they're ones we're already quite familiar with:
Edit: ... and, this capability impl_scope! {
#[impl_default]
struct Copse {
tree_type: Tree,
number: u32 = 7,
}
} |
@dhardy Cool to have that in a crate! I'd rather not do that plan in the language, though, since it'd make it harder to make those |
It's maybe handy if |
Rendered.