Skip to content
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 values for struct fields #1

Closed
nrc opened this issue Oct 4, 2016 · 18 comments
Closed

Default values for struct fields #1

nrc opened this issue Oct 4, 2016 · 18 comments

Comments

@nrc
Copy link
Owner

nrc commented Oct 4, 2016

Specify a default value for fields so that they don't need to be given in struct initialisers.

Points to consider:

  • syntax
  • type inference for field types?
  • which values are allowed as defaults
  • if a field implements Default, can we use that value somehow?
  • interaction with derive(Default)
  • interaction with .. in initialisers
  • some overlap with default arguments/keyword arguments

Previous discussion

Working directory: default-fields

@KodrAus
Copy link
Contributor

KodrAus commented Oct 5, 2016

Is there a key pain point or points this feature aims to address? I think the use case for it would affect whether its applied implicitely or explicitely.

If the goal is to make it possible to add to structs without breaking, then an implicit use makes sense. Wheras if the goal is to just make it possible to build structs more orgonomically then either an implicit or explicit (like ..) could work.

Then there's the question raised about named args in fns, and how that could benefit from this. I guess this RFC would just be targetting struct field defaults, with possible consideration for a future one for fns?

@nrc
Copy link
Owner Author

nrc commented Oct 5, 2016

For me, the main motivation is ergonomics. I think that back-compat is also a good motivation, and relies on the modularity improvements this brings (it would mean you could have a publicly instantiable struct with private fields).

re named args in fns, the link is really by analogy at this stage - there is an obvious correspondence between fn args and tuples, and between named args and anonymous structs. If structs have deafults for fields, then the analogy extends to default arguments. Since there is no concrete proposal for named or default args at the moment, this is all theoretical, however, I think it is worth thinking about in the design here.

@leoyvens
Copy link

leoyvens commented Oct 5, 2016

The big thing I see here is using struct expressions as the public API for intializing a struct instead of builders or constructors. This is flexible (vs constructors) and ergonomic (vs builders) for structs that don't need complicated intialization logic and wish to be future-proof. This would require not only default values for fields but default expressions. Rough discussion on specific points:

Syntax? Same as let bindings, field_name : type = expression.

Type inference for fields? No, require a type annotation. Otherwise rustdoc would have to perform type inference in order to document the type of a field!

Which values are allowed as defaults? Allowing only constants would narrow the space of types that can take a default value. To make this feature broadly useful the default value should be generalized to a default expression.

If a field implements Default, can we use that value somehow? You can use Default::default() as the expression.

Interaction with .. in initialisers? An initialiser that uses default expressions must end with , .. }. Default expressions are evaluated on struct initialisation. This should work even if Foo has private fields as long as they have default expressions. This is the awesome part, now it's easy to have a future-proof struct without resorting to a builder, just have a private field with a default expression and users will need to end their initialisers with , .. }. Allowing private fields is different from how FRU works and I don't know how hairy this is implementation wise.

Interaction with derive(Default)? It should prefer the default expression over Default::default().

Some overlap with default arguments/keyword arguments? This serves a similar use case but is less general and somewhat orthogonal, restricts the added complexity to struct initialisation. Hopefully also less contentious. Sets precedent regarding syntax for defaults. Big overlap with annonymous structs proposal. Also, is there overlap with fields in traits?

If the very rough design described here makes any sense, I'm willing to collaborate in some degree with an RFC.

@nrc
Copy link
Owner Author

nrc commented Oct 5, 2016

Worth thinking about const expressions as an intermediate between values and full expressions. Worth thinking about any similarities and differences between field defaults and consts/statics.

This is flexible (vs constructors) and ergonomic (vs builders)

I think that a single default for a field means we can't match builders for expressivity, but compares well with ctors.

Type inference for fields? No, require a type annotation. Otherwise rustdoc would have to perform type inference in order to document the type of a field!

This seems worth discussing - there is an explictness argument to be made for requiring types, but global type inference is not required (unlike say function arguments). Rustdoc already uses the compiler, so I don't think having to do type inference will be too problematic.

Which values are allowed as defaults? Allowing only constants would narrow the space of types that can take a default value. To make this feature broadly useful the default value should be generalized to a default expression.

There is a trade-off here certainly, and it seems worth discussing. Would be good to see examples.

Some questions: when would the expression be executed? Does it have inputs? Could the compiler optimise it sometimes? Is there a surpising cost here?

If a field implements Default, can we use that value somehow? You can use Default::default() as the expression.

This also depends on what we allow as the expression. It has been brought up elsewhere that we might want special syntax for this, although that is probably only necessary if the expression is not valid.

Interaction with .. in initialisers? An ...

Again, worth some discussion. There is certainly a trade-off between implicit and explicit. What about using .. expr } - should the value of fields be taken from expr or the default when there is a choice? Does there need to be a way to get the other option?

Sets precedent regarding syntax for defaults

And is that syntax reasonable? Worth exploring with some examples.

Also, is there overlap with fields in traits?

Would we allow defaults there? What would be the meaning?

@KodrAus
Copy link
Contributor

KodrAus commented Oct 6, 2016

I feel like the best way to progress would be to come up with some concrete examples, and deal with edge cases as they arise. @nrc how do you go from discussion that may end up running in circles indefinitely to a concrete proposal that anchors that discussion?

@nrc
Copy link
Owner Author

nrc commented Oct 6, 2016

@KodrAus making some examples sounds like a good way to start - I'd draw some up and check them in to the directory. I hesitate to recommend steps beyond that in terms of design because examples often throw out lots of useful info.

I'd also start drafting the motivation. Having a firm idea of the motivation for a feature is a good way to clarify design discussion.

@KodrAus
Copy link
Contributor

KodrAus commented Oct 6, 2016

Gotcha. Well there's lots of feedback around motivations, benefits and drawbacks floating around in the discussion links you've posted, so I'll spend some time this evening pulling that together.

@nrc
Copy link
Owner Author

nrc commented Oct 6, 2016

@KodrAus Could you submit as a PR please? It is easier to give feedback that way.

@leoyvens
Copy link

leoyvens commented Oct 6, 2016

Worth thinking about const expressions as an intermediate between values and full expressions. Worth thinking about any similarities and differences between field defaults and consts/statics.

I'll explore something between consts and any expression.

Consider String, Vec or HashMap. They have a straightforward Default impl but that impl can't be expressed as a const expression or even const fn AFAIK. Yet their pervasiveness motivates me to want fields of these types to be able to take their own default() as a default value. A (naive) solution would be to allow Default::default() as the expression, but that is functionally equivalent to allowing any expression since a user defined default() is free to do whatever. So we want to allow default() as a default value, but only reasonable impls of default().

What if we required that for a default() to be used as a default value it must be annotated with #[reasonable], a strawman syntax. Derived default() is reasonable if all fields of the struct have reasonable default().

But when can we mark a default as reasonable? May it allocate, panic, do I/O? Can this be checked or is it audited? If it is audited, should it be allowed outside of std? Could the definition of const fn expand so that it contains this notion of reasonable? Could fns other than default() be marked as reasonable? If this is all indeed so reasonable, do we need explicit syntax for using defaults or can it be implicit?

What about using .. expr } - should the value of fields be taken from expr or the default when there is a choice? Does there need to be a way to get the other option?

I would find it very surprising if .. expr } took values from anything other than expr. That means it's a no go for structs with private fields, but that's a somewhat orthogonal issue of FRU.

@nrc
Copy link
Owner Author

nrc commented Oct 6, 2016

There was a suggestion in one of the previous discussions about using a special syntax for Default::default(). That has the downside of needing a lang item, and being very special case, but I agree that it does seem a reasonable thing to want to do.

I'm interested on this point about discussing the relative merits of default values vs constructors - a ctor function which only takes some fields and has defaults for others is the current way to emulate field defaults. Part of the motivation discussion must be about where one would prefer a ctor function and where one would use default fields. In particular, a ctor allows initialising things like Vec. It might be that the line is no runtime overhead - in which case allowing Default::default does not make sense. Or it might be that that is not powerful enough to be useful.

I don't think one can change the definition of const fn away from being a compile-time executed function.

@KodrAus
Copy link
Contributor

KodrAus commented Oct 6, 2016

I started leaning towards const after spending some time playing with the implications of allowing any expression in the field value. Take the following example:

pub struct Data {
    pub value: bool,
    private_stuff: () = {
        let client = hyper::Client::new();
        let value = client.do_some_http();
        value.unwrap();

    }
}

Which has a few problems (apart from being junky code):

  • The struct does hard work in a field the caller knows nothing about
  • The struct may panic on construction which is totally unexpected
  • Even if value was a Result it's still doing lots of work

It would make me feel dubious about building structs because I don't know what it could be doing in the background. Limiting values to constant expressions mitigates the weirdness someone could do, which is necessary if this is a feature that requires no special syntax.

If it did require special syntax though, then I'd be totally ok with it doing strange things because I've opted in to that behaviour.

So I think that's what differentiates value defaults from ctors. You expect a function to do things, and you can use an appropriate type when errors come up, because that type is surfaced to the user.

The unfortunate case are those heap collections like @leodasvacas mentions. If not being able to default them makes this feature useless, then maybe it should allow arbitrary expressions, and then use an opt-in syntax. Oh the tradeoffs!

@leoyvens
Copy link

leoyvens commented Oct 6, 2016

@nrc For one thing the number of required ctors blows up if you want the flexibility of having all combinations like you have in struct expressions, each field doubles the number of possible ctors. But yes that is an important discussion to add to the motivation.

I don't think one can change the definition of const fn away from being a compile-time executed function.

Vec::new() dosen't actually allocate anything, in theory couldn't it use CTFE?

@KodrAus Agree with your view on the tradeoffs.

@nrc
Copy link
Owner Author

nrc commented Oct 7, 2016

For one thing the number of required ctors blows up if you want the flexibility of having all combinations like you have in struct expressions

You bring up an important point - it is the fact that default fields are overridable (at least if they're not private) which makes them more powerful than ctor functions.

Vec::new() dosen't actually allocate anything, in theory couldn't it use CTFE?

This might be worth investigating. I'm not sure if it is possible to make it const, and if it is, I'm not sure if it is backwards compatible, but if it is possible, then it seems like more reason to stick to const fns. Worth discussing in the RFC and some further investigation.

@KodrAus
Copy link
Contributor

KodrAus commented Oct 7, 2016

So:

  • Builders give you the best flexibility for dealing with complexity, but there's boilerplate that needs to be set up and maintained.
  • Constructor functions require less boilerplate than builders, allow complex logic, but don't scale so well (they'll quickly become much more bloated than builders if you need to support alternatives).
  • Default fields require basically no extra effort, but you're limited to const values.

Is that state of affairs acceptable? Or is there some alternative implementation (either loosening the const requirement, or exploring ways to make const functions cover more values) that gives you the benefits of constructor functions and default fields without the drawbacks?

Personally, I like the idea of getting more things constable (can we keep that pun in the RFC?), because that benefits more people... But could be impossible or unreasonable for some important types.

@nrc
Copy link
Owner Author

nrc commented Oct 9, 2016

@KodrAus this all sounds good to me. We might also consider some sugar for supporting default() as a field default. Although this violates the expectation of cheap initialisation, there is some expectation that default() is cheap. Worth discussion I think, not sure which side of the fence I am on there.

@KodrAus
Copy link
Contributor

KodrAus commented Oct 9, 2016

Personally, I'm not opposed to allowing default() because as you say, there's an expectation that it's cheap. It's kind of a less formal approach than @leodasvacas's #[reasonable] idea and makes the feature more powerful.

@KodrAus
Copy link
Contributor

KodrAus commented Dec 3, 2016

Ok! I've opened a PR in the main RFC repo for default fields.

It's been a great going through the design of a new language feature, I'm looking forward to seeing it through the main RFC process.

Thanks @nrc and @leodasvacas! I'm keen to slowly ramp up my contribution to Rust proper.

@nrc
Copy link
Owner Author

nrc commented Dec 5, 2016

Awesome, so, I believe this issue is done (well, not really till the RFC is accepted (or not), but I think there is no more to do here).

@nrc nrc closed this as completed Dec 5, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants