Proto-RFC: Expanded functional record update


#1
  • 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:

  1. The behavior should not change depending on field privacy. (That would just be weird.)
  2. But the behavior needs to be backwards compatible in all cases where FRU works now, including whether Drop is called.
  3. 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 entire foo 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?


#2

I made some prototype for the alternative FRU desugaring (https://github.com/petrochenkov/rust/tree/fru).

As you can see the breakage is pretty significant (see the second commit).
In addition there are cases where the current semantics is actually desirable.

  • FRU from borrowed contexts, you can’t move the whole value, but can copy individual Copy fields.
  • Situations when some fields are simply copied, but others are somehow copied-and-transformed (see e.g. fold.rs in my patch). This cannot be done if the whole old value is moved out at the start.
  • Emulating Copy for types that can be Copy but don’t implement it for semantic reasons (e.g. iterators).

So the alternative semantics for FRU has to use some syntax distinct from the current one, and the current semantics needs to be kept.

If RFC 2061 is accepted and individual fields in Drop structs become consumable, then the only remaining major issue with the current semantics would be field privacy.