|
| 1 | +- Feature Name: `type_changing_struct_update_syntax` |
| 2 | +- Start Date: 2018-08-22 |
| 3 | +- RFC PR: https://github.com/rust-lang/rfcs/pull/2528 |
| 4 | +- Rust Issue: https://github.com/rust-lang/rust/issues/86555 |
| 5 | + |
| 6 | +# Summary |
| 7 | +[summary]: #summary |
| 8 | + |
| 9 | +Extend struct update syntax (a.k.a. functional record update (FRU)) to support |
| 10 | +instances of the *same* struct that have different types due to generic type or |
| 11 | +lifetime parameters. Fields of different types must be explicitly listed in the |
| 12 | +struct constructor, but fields of the same name and same type can be moved with |
| 13 | +struct update syntax. |
| 14 | + |
| 15 | +This will make the following possible. In this example, `base` and `updated` |
| 16 | +are both instances of `Foo` but have different types because the generic |
| 17 | +parameter `T` is different. Struct update syntax is supported for `field2` |
| 18 | +because it has the same type `i32` in both `base` and `updated`: |
| 19 | + |
| 20 | +```rust |
| 21 | +struct Foo<T, U> { |
| 22 | + field1: T, |
| 23 | + field2: U, |
| 24 | +} |
| 25 | + |
| 26 | +let base: Foo<String, i32> = Foo { |
| 27 | + field1: String::from("hello"), |
| 28 | + field2: 1234, |
| 29 | +}; |
| 30 | +let updated: Foo<f64, i32> = Foo { |
| 31 | + field1: 3.14, |
| 32 | + ..base |
| 33 | +}; |
| 34 | +``` |
| 35 | + |
| 36 | +# Motivation |
| 37 | +[motivation]: #motivation |
| 38 | + |
| 39 | +In today's Rust, struct update syntax is a convenient way to change a small |
| 40 | +number of fields from a base instance as long as the updated instance is a |
| 41 | +subtype of the base (i.e. the *exact same* type except lifetimes). However, |
| 42 | +this is unnecessarily restrictive. A common pattern for implementing |
| 43 | +type-checked state machines in Rust is to handle the state as a generic type |
| 44 | +parameter. For example: |
| 45 | + |
| 46 | +```rust |
| 47 | +struct Machine<S> { |
| 48 | + state: S, |
| 49 | + common_field1: &'static str, |
| 50 | + common_field2: i32, |
| 51 | +} |
| 52 | + |
| 53 | +struct State1; |
| 54 | +struct State2; |
| 55 | + |
| 56 | +impl Machine<State1> { |
| 57 | + fn into_state2(self) -> Machine<State2> { |
| 58 | + // do stuff |
| 59 | + Machine { |
| 60 | + state: State2, |
| 61 | + common_field1: self.common_field1, |
| 62 | + common_field2: self.common_field2, |
| 63 | + } |
| 64 | + } |
| 65 | +} |
| 66 | +``` |
| 67 | + |
| 68 | +It would be much more convenient to be able to write |
| 69 | + |
| 70 | +```rust |
| 71 | +Machine { |
| 72 | + state: State2, |
| 73 | + ..self |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +instead of |
| 78 | + |
| 79 | +```rust |
| 80 | +Machine { |
| 81 | + state: State2, |
| 82 | + common_field1: self.common_field1, |
| 83 | + common_field2: self.common_field2, |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +but this is not possible in current Rust because `Machine<State1>` and |
| 88 | +`Machine<State2>` are different types even though they are both the `Machine` |
| 89 | +struct. |
| 90 | + |
| 91 | +# Guide-level explanation |
| 92 | +[guide-level-explanation]: #guide-level-explanation |
| 93 | + |
| 94 | +It's often useful to create a new instance of a struct that uses most of an old |
| 95 | +instance's values but changes some. You can do this using struct update syntax. |
| 96 | + |
| 97 | +Consider a `User` type that can be in either the `LoggedIn` state or the |
| 98 | +`LoggedOut` state and has a few additional fields describing the properties of |
| 99 | +the user. |
| 100 | + |
| 101 | +```rust |
| 102 | +struct User<S> { |
| 103 | + state: S, |
| 104 | + email: String, |
| 105 | + username: String, |
| 106 | +} |
| 107 | + |
| 108 | +struct LoggedIn; |
| 109 | +struct LoggedOut; |
| 110 | +``` |
| 111 | + |
| 112 | +Let's say we have a logged-out user: |
| 113 | + |
| 114 | +```rust |
| 115 | +let logged_out = User { |
| 116 | + state: LoggedOut, |
| 117 | + email: String::from( "[email protected]"), |
| 118 | + username: String::from("ferris"), |
| 119 | +}; |
| 120 | +``` |
| 121 | + |
| 122 | +This example shows how we create a new `User` instance named `logged_in` |
| 123 | +without the update syntax. We set a new value for `state` but move the values |
| 124 | +of the other fields from `logged_out`. |
| 125 | + |
| 126 | +```rust |
| 127 | +let logged_in = User { |
| 128 | + state: LoggedIn, |
| 129 | + email: logged_out.email, |
| 130 | + username: logged_out.username, |
| 131 | +}; |
| 132 | +``` |
| 133 | + |
| 134 | +Using struct update syntax, we can achieve the same effect more concisely, as |
| 135 | +shown below. The syntax `..` specifies that the remaining fields not explicitly |
| 136 | +set should be moved from the fields of the base instance. |
| 137 | + |
| 138 | +```rust |
| 139 | +let logged_in = User { |
| 140 | + state: LoggedIn, |
| 141 | + ..logged_out |
| 142 | +}; |
| 143 | +``` |
| 144 | + |
| 145 | +Note that the expression following the `..` is an *expression*; it doesn't have |
| 146 | +to be just an identifier of an existing instance. For example, it's often |
| 147 | +useful to use struct update syntax with `..Default::default()` to override a |
| 148 | +few field values from their default. |
| 149 | + |
| 150 | +Struct update syntax is permitted for instances of the *same* struct (`User` in |
| 151 | +the examples), even if they have different types (`User<LoggedOut>` and |
| 152 | +`User<LoggedIn>` in the examples) due to generic type or lifetime parameters. |
| 153 | +However, the types of the fields in the updated instance that are not |
| 154 | +explicitly listed (i.e. those that are moved with the `..` syntax) must be |
| 155 | +subtypes of the corresponding fields in the base instance, and all of the |
| 156 | +fields must be visible ([RFC 736]). In other words, the types of fields that |
| 157 | +are explicitly listed can change, such as the `state` field in the examples, |
| 158 | +but those that are not explicitly listed, such as the `email` and `username` |
| 159 | +fields in the examples, must stay the same (modulo subtyping). |
| 160 | + |
| 161 | +Existing Rust programmers can think of this RFC as extending struct update |
| 162 | +syntax to cases where some of the fields change their type, as long as those |
| 163 | +fields are explicitly listed in the struct constructor. |
| 164 | + |
| 165 | +# Reference-level explanation |
| 166 | +[reference-level-explanation]: #reference-level-explanation |
| 167 | + |
| 168 | +Struct update syntax is now allowed for instances of the *same* struct even if |
| 169 | +the generic type parameters or lifetimes of the struct are different between |
| 170 | +the base and updated instances. The following conditions must be met: |
| 171 | + |
| 172 | +1. The base and updated instances are of the same struct. |
| 173 | + |
| 174 | +2. The type of each moved field (i.e. each field not explicitly listed) in the |
| 175 | + updated instance is a subtype of the type of the corresponding field in the |
| 176 | + base instance. |
| 177 | + |
| 178 | +3. All fields are visible at the location of the update ([RFC 736]). |
| 179 | + |
| 180 | +The struct update syntax is the following: |
| 181 | + |
| 182 | +```rust |
| 183 | +$struct_name:path { |
| 184 | + $($field_name:ident: $field_value:expr,)* |
| 185 | + ..$base_instance:expr |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Struct update syntax is directly equivalent to explicitly listing all of the |
| 190 | +fields, with the possible exception of type inference. For example, the listing |
| 191 | +from the previous section |
| 192 | + |
| 193 | +```rust |
| 194 | +let logged_in = User { |
| 195 | + state: LoggedIn, |
| 196 | + ..logged_out |
| 197 | +}; |
| 198 | +``` |
| 199 | + |
| 200 | +is directly equivalent to |
| 201 | + |
| 202 | +```rust |
| 203 | +let logged_in = User { |
| 204 | + state: LoggedIn, |
| 205 | + email: logged_out.email, |
| 206 | + username: logged_out.username, |
| 207 | +}; |
| 208 | +``` |
| 209 | + |
| 210 | +except, possibly, for type inference. |
| 211 | + |
| 212 | +# Drawbacks |
| 213 | +[drawbacks]: #drawbacks |
| 214 | + |
| 215 | +There are trade-offs to be made when selecting the type inference strategy, |
| 216 | +since the types of fields are no longer necessarily the same between the base |
| 217 | +and updated instances in struct update syntax. See the *Type inference* section |
| 218 | +under [Unresolved questions](#unresolved-questions). |
| 219 | + |
| 220 | +# Rationale and alternatives |
| 221 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 222 | + |
| 223 | +This proposal is a relatively small user-facing generalization that |
| 224 | +significantly improves language ergonomics in some cases. |
| 225 | + |
| 226 | +## Further generalization |
| 227 | + |
| 228 | +This proposal maintains the restriction that the types of the base and updated |
| 229 | +instance must be the same struct. Struct update syntax could be further |
| 230 | +generalized by lifting this restriction, so that the only remaining restriction |
| 231 | +would be that the moved field names and types must match. For example, the |
| 232 | +following could be allowed: |
| 233 | + |
| 234 | +```rust |
| 235 | +struct Foo { |
| 236 | + field1: &'static str, |
| 237 | + field2: i32, |
| 238 | +} |
| 239 | + |
| 240 | +struct Bar { |
| 241 | + field1: f64, |
| 242 | + field2: i32, |
| 243 | +} |
| 244 | + |
| 245 | +let foo = Foo { field1: "hi", field2: 1 }; |
| 246 | +let bar = Bar { field1: 3.14, ..foo }; |
| 247 | +``` |
| 248 | + |
| 249 | +While this would be convenient in some cases, it makes field names a much more |
| 250 | +important part of the crate's API. It could also be considered to be too |
| 251 | +implicit. |
| 252 | + |
| 253 | +The proposal in this RFC does not preclude this further generalization in the |
| 254 | +future if desired. The further generalization could be applied in a manner that |
| 255 | +is backwards-compatible with this RFC. As a result, the conservative approach |
| 256 | +presented in this RFC is a good first step. After the community has experience |
| 257 | +with this proposal, further generalization may be considered in the future. |
| 258 | + |
| 259 | +## Keep the existing behavior |
| 260 | + |
| 261 | +If we decide to keep the existing behavior, we are implicitly encouraging users |
| 262 | +to handle more logic with runtime checks so that they can use the concise |
| 263 | +struct update syntax instead of the verbose syntax required due to type |
| 264 | +changes. By implementing this RFC, we improve the ergonomics of using the type |
| 265 | +system to enforce constraints at compile time. |
| 266 | + |
| 267 | +# Prior art |
| 268 | +[prior-art]: #prior-art |
| 269 | + |
| 270 | +OCaml and Haskell allow changing the type of generic parameters with functional |
| 271 | +record update syntax, like this RFC. |
| 272 | + |
| 273 | +* OCaml: |
| 274 | + |
| 275 | + ```ocaml |
| 276 | + # type 'a foo = { a: 'a; b: int };; |
| 277 | + type 'a foo = { a : 'a; b : int; } |
| 278 | + # let x: int foo = { a = 5; b = 6 };; |
| 279 | + val x : int foo = {a = 5; b = 6} |
| 280 | + # let y: float foo = { x with a = 3.14 };; |
| 281 | + val y : float foo = {a = 3.14; b = 6} |
| 282 | + ``` |
| 283 | + |
| 284 | +* Haskell: |
| 285 | + |
| 286 | + ```haskell |
| 287 | + Prelude> data Foo a = Foo { a :: a, b :: Int } |
| 288 | + Prelude> x = Foo { a = 5, b = 6 } |
| 289 | + Prelude> :type x |
| 290 | + x :: Num a => Foo a |
| 291 | + Prelude> y = x { a = 3.14 } |
| 292 | + Prelude> :type y |
| 293 | + y :: Fractional a => Foo a |
| 294 | + ``` |
| 295 | + |
| 296 | +Like this RFC, OCaml does not allow the alternative further generalization: |
| 297 | + |
| 298 | +```ocaml |
| 299 | +# type foo = { a: int; b: int };; |
| 300 | +type foo = { a : int; b : int; } |
| 301 | +# type bar = { a: int; b: int };; |
| 302 | +type bar = { a : int; b : int; } |
| 303 | +# let x: foo = { a = 5; b = 6 };; |
| 304 | +val x : foo = {a = 5; b = 6} |
| 305 | +# let y: bar = { x with a = 7 };; |
| 306 | +File "", line 1, characters 15-16: |
| 307 | +Error: This expression has type foo but an expression was expected of type |
| 308 | + bar |
| 309 | +``` |
| 310 | + |
| 311 | +# Unresolved questions |
| 312 | +[unresolved-questions]: #unresolved-questions |
| 313 | + |
| 314 | +## Type inference |
| 315 | + |
| 316 | +What is the best type inference strategy? In today's Rust, the types of the |
| 317 | +explicitly listed fields are always the same in the base and updated instances. |
| 318 | +With this RFC, the types of the explicitly listed fields can be different |
| 319 | +between the base and updated instances. This removes some of the constraints on |
| 320 | +type inference compared to today's Rust. There are choices to make regarding |
| 321 | +backwards compatibility of inferred types, the `i32`/`f64` fallback in type |
| 322 | +inference, and the conceptual simplicity of the chosen strategy. |
| 323 | + |
| 324 | +## Further generalization |
| 325 | + |
| 326 | +Should struct update syntax be further generalized to ignore the struct type |
| 327 | +and just consider field names and field types? This question could be answered |
| 328 | +later after users have experience with the changes this RFC. The further |
| 329 | +generalization could be implemented in a backwards-compatible way. |
| 330 | + |
| 331 | +[RFC 736]: https://github.com/rust-lang/rfcs/blob/master/text/0736-privacy-respecting-fru.md |
0 commit comments