|
| 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