Skip to content

Commit 651d8e7

Browse files
committed
Merge branch 'type-changing-struct-update-syntax'
2 parents 4904e05 + 8c817c9 commit 651d8e7

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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

Comments
 (0)