Skip to content

Commit c8e2899

Browse files
nate-chandlerbenrimmingtonxwu
authored
Partial Consumption of Noncopyable Values (#2317)
* Draft * [PartialConsumption] Permit within deinit. * [PartialConsumption] Add per-field consumption. * [PartialConsumption] Imported not resilient. Apply the rules required for resilient types universally. * [PartialConsumption] Tweak example. * [PartialConsumption] Copyable future directions. * [PartialConsumption] Minor cleanup. Grammar and markdown styling. * [PartialConsumption] State visibility keywords. * [PartialConsumption] Fix swap example. Co-authored-by: Ben Rimmington <[email protected]> * Prepare SE-0429 for review --------- Co-authored-by: Ben Rimmington <[email protected]> Co-authored-by: Xiaodi Wu <[email protected]>
1 parent 88e80ad commit c8e2899

File tree

1 file changed

+336
-0
lines changed

1 file changed

+336
-0
lines changed

proposals/0429-partial-consumption.md

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
# Partial consumption of noncopyable values
2+
3+
* Proposal: [SE-0429](0429-partial-consumption.md)
4+
* Authors: [Michael Gottesman](https://github.com/gottesmm), [Nate Chandler](https://github.com/nate-chandler)
5+
* Review Manager: [Xiaodi Wu](https://github.com/xwu)
6+
* Implementation: On `main` gated behind `-enable-experimental-feature MoveOnlyPartialConsumption`
7+
* Status: **Active Review (March 13...26, 2024)**
8+
* Review: ([pitch #1](https://forums.swift.org/t/request-for-feedback-partial-consumption-of-fields-of-noncopyable-types/65884)) ([pitch #2](https://forums.swift.org/t/pitch-piecewise-consumption-of-noncopyable-values/70045)) ([review]())
9+
<!-- * Upcoming Feature Flag: `MoveOnlyPartialConsumption` -->
10+
11+
## Introduction
12+
13+
We propose allowing noncopyable fields in deinit-less aggregates to be consumed individually,
14+
so long as they are defined in the current module or frozen.
15+
Additionally, we propose allowing fields of such an aggregate with a deinit to be consumed individually _within that deinit_.
16+
This permits common patterns to be used with many noncopyable values.
17+
18+
## Motivation
19+
20+
In Swift today, it can be challenging to manipulate noncopyable fields of an aggregate.
21+
22+
For example, consider a `Pair` of noncopyable values:
23+
24+
```swift
25+
struct Unique : ~Copyable {...}
26+
struct Pair : ~Copyable {
27+
let first: Unique
28+
let second: Unique
29+
}
30+
```
31+
32+
It is currently not straightforward to write a function that forms a new `Pair` with the values reversed.
33+
For example, the following code is not currently allowed:
34+
35+
```swift
36+
extension Pair {
37+
consuming func swap() -> Pair {
38+
return Pair(
39+
first: second, // error: cannot partially consume 'self'
40+
second: first // error: cannot partially consume 'self'
41+
)
42+
}
43+
}
44+
```
45+
46+
There are various workarounds for this, but they are not ideal.
47+
48+
## Proposed solution
49+
50+
We allow noncopyable aggregates without deinits to be consumed field-by-field, if they are defined in the current module or frozen.
51+
That makes `swap` above legal as written.
52+
53+
This initial proposal is deliberately minimal:
54+
- We do not allow partial consumption of [noncopyable aggregates that have deinits](#future-direction-discard).
55+
- We do not support [reinitializing](#future-direction-partial-reinitialization) fields after they are consumed.
56+
57+
[Imported aggregates](#imported-aggregates) can never be partially consumed, unless they are frozen.
58+
59+
## Detailed design
60+
61+
We relax the requirement that a noncopyable aggregate be consumed at most once on each path.
62+
Instead we require only that each of its noncopyable fields be consumed at most once on each path.
63+
Imported aggregates (i.e. those defined in another module and marked either `public` or `package`), however, cannot be partially consumed unless they are marked `@frozen`.
64+
65+
Extending the `Pair` example above, the following becomes legal:
66+
67+
```swift
68+
func takeUnique(_ elt: consuming Unique) {}
69+
extension Pair {
70+
consuming func passUniques(_ forward: Bool) {
71+
if forward {
72+
takeUnique(first)
73+
takeUnique(second)
74+
} else {
75+
takeUnique(second)
76+
takeUnique(first)
77+
}
78+
}
79+
}
80+
```
81+
82+
The struct `Pair` has two noncopyable fields, `first` and `second`.
83+
And there are two paths through the function: the paths taken when `forward` is `true` and when it is `false`.
84+
On both paths, `first` and `second` are both consumed exactly once.
85+
86+
It's not necessary to consume every field on every path, however.
87+
For example, the following is allowed as well:
88+
89+
```swift
90+
extension Pair {
91+
consuming func passUnique(_ front: Bool) {
92+
if front {
93+
takeUnique(first)
94+
} else {
95+
takeUnique(second)
96+
}
97+
}
98+
}
99+
```
100+
101+
Here, only `first` is consumed on the path taken when `front` is `true` and only `second` on that taken when `front` is `false`.
102+
103+
### Field lifetime extension<a name="lifetime-extension"/>
104+
105+
When a field is _not_ consumed on some path, its destruction is deferred as long as possible.
106+
Here, that looks like this:
107+
108+
```swift
109+
extension Pair {
110+
consuming func passUnique(_ front: Bool) {
111+
if front {
112+
takeUnique(first)
113+
// second is destroyed
114+
} else {
115+
takeUnique(second)
116+
// first is destroyed
117+
}
118+
}
119+
}
120+
```
121+
122+
Neither `first` nor `second` can be destroyed _after_ the `if`/`else` blocks because that would require a copy.
123+
124+
### Explicit field consumption
125+
126+
Fields can also be consumed explicitly via the `consume` keyword.
127+
This enables overriding the [extension of a field's lifetime](#lifetime-extension).
128+
129+
Continuing the example, if it were necessary that `first` always be destroyed before `second`, the following could be written:
130+
131+
```swift
132+
extension Pair {
133+
consuming func passUnique(_ front: Bool) {
134+
if front {
135+
takeUnique(first)
136+
// second is destroyed
137+
} else {
138+
_ = consume first
139+
takeUnique(second)
140+
}
141+
}
142+
}
143+
```
144+
145+
### Imported aggregates<a name="imported-aggregates"/>
146+
147+
Partial consumption of a non-copyable type is always allowed when the type is defined in the module where it is consumed.
148+
If the type is defined in another module, partial consumption is only permitted if the type is marked `@frozen`.
149+
150+
The reason for this limitation is that as the module defining a type changes,
151+
the type itself may change, adding or removing fields, changing fields to computed properties, and so on.
152+
A partial consumption of the type's fields that makes sense as the type is defined by one version of the module
153+
may not make sense as the type is defined in another version.
154+
That consideration does not apply to frozen types, however,
155+
because by marking them `@frozen`, the module's author promises not to change their layouts.
156+
157+
These rules are unavoidable for libraries built with library evolution
158+
and are applied universally to avoid having language rules differ based on the build mode.
159+
160+
### Copyable fields
161+
162+
It is currently legal to have multiple consuming uses of a copyable field of a noncopyable aggregate.
163+
For example:
164+
165+
```swift
166+
func takeString(_ name: consuming String) {}
167+
struct Named : ~Copyable {
168+
let unique: Unique
169+
let name: String
170+
consuming func use() {
171+
takeString(name)
172+
takeString(name)
173+
takeString(name)
174+
takeString(name)
175+
// unique is consumed
176+
}
177+
}
178+
```
179+
180+
This remains true when a value is partially consumed:
181+
182+
```swift
183+
extension Named {
184+
consuming func unpack() {
185+
takeString(name)
186+
takeString(name)
187+
takeUnique(unique)
188+
takeString(name)
189+
takeString(name)
190+
}
191+
}
192+
```
193+
194+
### Partial consumption within deinits
195+
196+
There are two related reasons to limit partial consumption to fields of types without deinits:
197+
First, the deinit of such types can't be run if it is partially consumed.
198+
Second, no proposed mechanism to indicate that the deinit should not be run has been accepted.
199+
200+
Neither applies when partially consuming a value within its own deinit.
201+
We propose allowing a value to be partially consumed there.
202+
203+
```swift
204+
struct Pair2 : ~Copyable {
205+
let first: Unique
206+
let second: Unique
207+
208+
deinit {
209+
takeUnique(first) // partially consumes self
210+
takeUnique(second) // partially consumes self
211+
}
212+
}
213+
```
214+
215+
This enables noncopyable structs to dispose of any resources they own on destruction.
216+
217+
## Source compatibility
218+
219+
No effect.
220+
The proposal makes more code legal.
221+
222+
## ABI compatibility
223+
224+
No effect.
225+
226+
## Implications on adoption
227+
228+
This proposal makes more code legal.
229+
And the code it makes legal is code written in a style familiar to Swift developers used to working with copyable values.
230+
It alleviates some pain points associated with writing noncopyable code, easing further adoption.
231+
232+
## Future directions
233+
234+
### Discard<a name="future-direction-discard"/>
235+
236+
This document proposes limiting partial consumption to aggregates without deinit.
237+
In the future, another proposal could lift that restriction.
238+
The trouble with lifting it is that the deinit can no longer be run, which may be surprising.
239+
That trouble could be mitigated by requiring the value be `discard`'d prior to partial consumption,
240+
indicating that the deinit should not be run.
241+
242+
```swift
243+
struct Box : ~Copyable {
244+
var unique: Unique
245+
deinit {...}
246+
247+
consuming func unpack() -> Unique {
248+
discard self
249+
return unique
250+
}
251+
}
252+
```
253+
254+
### Partial reinitialization<a name="future-direction-partial-reinitialization"/>
255+
256+
This document only proposes allowing the fields of an aggregate to be consumed individually.
257+
It does not allow for those fields to be _reinitialized_ in order to return the aggregate to a legal state.
258+
In the future, though, another proposal could lift that restriction.
259+
260+
That would enable further code patterns--already legal with copyable values--to be written in noncopyable contexts
261+
For example:
262+
263+
```swift
264+
struct Unique : ~Copyable {}
265+
struct Pair : ~Copyable {
266+
var first: Unique
267+
var second: Unique
268+
}
269+
270+
extension Pair {
271+
mutating func swap() {
272+
let tmp = first
273+
first = second
274+
second = tmp
275+
}
276+
}
277+
```
278+
279+
### Partial consumption of copyable fields
280+
281+
This document only proposes allowing the noncopyable fields of a noncopyable aggregate to be consumed individually.
282+
In the future, the ability to explicitly consume (via the `consume` keyword) the copyable fields of a copyable aggregate could be added.
283+
284+
```swift
285+
class C {}
286+
func takeC(_ c: consuming C)
287+
struct PairPlusC : ~Copyable {
288+
let first: Unique
289+
let second: Unique
290+
let c: C
291+
}
292+
293+
func disaggregate(_ p: consuming PairPlusC) {
294+
takeUnique(p.first)
295+
takeC(consume p.c) // p.c's lifetime ends
296+
takeUnique(p.second)
297+
}
298+
```
299+
300+
That would provide the ability to specify the point at which the lifetime of a copyable field should end.
301+
302+
### Partial consumption of copyable aggregates
303+
304+
This document only proposes allowing noncopyable aggregates to be partially consumed.
305+
There is a natural extension of this to copyable aggregates:
306+
307+
```swift
308+
class C {}
309+
struct CopyablePairOfCs {
310+
let c1: C
311+
let c2: C
312+
}
313+
func tearDownInOrder(_ p: consuming CopyablePairOfCs) {
314+
takeC(consume p.c2)
315+
takeC(consume p.c1)
316+
}
317+
```
318+
319+
## Alternatives considered
320+
321+
### Explicit destructuring
322+
323+
Instead of consuming the fields of a struct piecewise, an alternative would be to simultaneously bind every field to a variable:
324+
325+
```swift
326+
let (a, b) = destructure s
327+
```
328+
329+
Something like this might be desirable eventually, but it would be best introduced as part of support for pattern matching for structs.
330+
Even with such a feature, the behavior proposed here would remain desirable:
331+
fields of a copyable aggregate can be consumed field-by-field,
332+
so consuming fields of a noncopyable aggregate should be supported as much as possible too.
333+
334+
## Acknowledgments
335+
336+
Thanks to Andrew Trick for extensive design conversations and implementation review.

0 commit comments

Comments
 (0)