- Feature Name: expanded_fru
- Start Date: 2016-01-12
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
Change the semantics of functional record update to ignore privacy of transferred fields if the source either impls Copy
or is an rvalue that does not impl Drop
.
Motivation
Functional record update is the syntax ..<expr>
at the end of a struct literal, defined to set the remaining fields in the struct to the corresponding fields from <expr>
, which should be of the struct type. Currently this is taken fairly literally: the semantics of
Foo { ..foo }
are, as far as I know, the same as
Foo { field1: foo.field1, field2: foo.field2, <etc.> }
In particular, the form cannot be used if any of the remaining fields are inaccessible due to privacy (since RFC 736), and it cannot move any non-Copy
fields if the struct implements Drop (because that would leave the original in a half-formed state).
Unfortunately, as stated in that RFC, this prevents using FRU in many situations where it âought to workâ. For example, a common pattern to define âextensibleâ structs is to add a dummy private field to a struct otherwise consisting of public fields, in order to prevent module-external clients from directly constructing or destructuring the struct, which would break if new fields were added. We would like to be able to say
Foo { field1: xxx, ..Default::default() }
to specify some fields of the struct and leave the rest at the defaults, but this will break if the struct contains private fields. The RFC does mention a workaround involving making the dummy field public but giving it an unconstructible type, but that has several downsides: (a) more typing, (b) still allows clients to destructure the struct, © the hidden field shows up in docs.
The reason this âought to workâ is that we can write this to do roughly the same thing, which Iâll call âmove-then-updateâ:
{ let mut foo = Default::default(); foo.field1 = xxx; foo }
It would be nice if FRU could take advantage of these semantics. However, there are several constraints:
- The behavior should not change depending on field privacy. (That would just be weird.)
- But the behavior needs to be backwards compatible in all cases where FRU works now, including whether Drop is called.
- And it still must not allow any operation that would otherwise be impossible due to privacy.
With that in mind, here is a comparison of the current FRU semantics to what you would get by writing out move-then-update, depending on the types involved. Keep in mind that the non-transferred (specified in the struct literal) fields are necessarily public/accessible, but the transferred ones may be inaccessible.
-
Drop impl, but only Copy fields need to transfer:
- FRU: Works; original object intact.
- Move-then-update: original object invalidated (matters for lvalues only), so the objectâs Drop wonât be called (matters for rvalues too).
-
Drop impl, otherwise:
- FRU: Doesnât work, so backwards compatibility isnât a problem.
- Move-then-update: original object invalidated.
-
Copy impl (for entire object):
- Both âjust workâ.
-
No drop, only Copy fields need to transfer:
- FRU: Works; original object intact.
- Move-then-update: original object invalidated (matters for lvalues only). (Not invalidating it wouldnât be backwards incompatible per se, but would reintroduce the same ability to trick unsafe code that RFC 736 fixed.)
-
No drop, otherwise:
- FRU: Works; original object in partially moved state.
- Move-then-update: same as last case.
Thus the cases that could be theoretically made to work are (1) Copy objects, (2) non-drop-impled rvalues only, and (3) Drop-impled objects where non-Copy fields need to transfer.
The last of those should almost certainly be excluded, because supporting it would make FRU-ability potentially depend on the existence of private fields (the non-Copy ones), and in any case making transfers work only if some fields arenât copyable would be bizarre.
Implementing only the first would be simplest, but only a fraction of structs are Copy, so it wouldnât be all that useful.
Thus I propose supporting the first two.
Detailed design
Although the justification is based on whatâs possible with move-then-update, the version in the summary should be complete and semantically indistinguishable: FRU works as normal but ignores privacy in those cases. Iâm not familiar with rustc internals, but for this reason I donât think it should be difficult to implement.
Ideally, in FRU expressions where this rule does not apply, the diagnostics for (a) privacy violations and (b) moves out of Drop types should briefly mention this rule.
Drawbacks
- Supporting rvalues and not lvalues is odd. If the object the user wants to transfer from is an lvalue (e.g. a variable
foo
), they can turn it into an rvalue with{foo}
. This has the right semantics (in particular,{foo}.field
always moves the entirefoo
rather than doing a partial move), but itâs somewhat nonobvious how it works and why itâs necessary in any given case.
I think this feature will be used most often with the source specified as a direct call to some sort of constructor function, e.g. { <fields>, ..Default::default() }
, which will just work.
-
Drop objects are treated specially for no good reason other than backwards compatibility.
-
The semantics of FRU become somewhat more complicated, while currently it can be understood in terms of a single obvious destructuring.
Alternatives
-
Instead of compatibly extending the existing field-at-a-time semantics, introduce an alternate form that uses move-then-update. For example, using the
move
keyword:Foo { field1: xxx, ..move Default::default() }
would be equivalent to{ let mut foo = Default::default(); foo.field1 = xxx; foo }
. -
Donât support Copy lvalues (Copy rvalues necessarily fall under ârvalue that does not impl Dropâ). Simplifies the rule somewhat.
-
Do nothing; people can use the current alternatives, including writing out the field assignments, the unconstructible-public hack mentioned above, and (as a general alternative to having structs with lots of public fields to set) builder types.
-
Support Drop-impled rvalues, which would be a relatively minor stable-breaking change, or other breaking cases.
Unresolved questions
Are there any problematic oddities in the existing FRU semantics that Iâm unaware of?
This is my first RFC; am I missing something completely obvious?