Skip to content

Commit 19569f3

Browse files
committed
[SE-0258] Provide a more feasible future design for instance properties of classes
1 parent f5f5727 commit 19569f3

File tree

1 file changed

+35
-32
lines changed

1 file changed

+35
-32
lines changed

proposals/0258-property-wrappers.md

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,7 +1387,7 @@ public class MyClass: Superclass {
13871387
get { return backingMyVar }
13881388
set {
13891389
if newValue != backingMyVar {
1390-
self.broadcastValueChanged(oldValue: backingMyVar, newValue: newValue)
1390+
self.broadcastValueWillChange(newValue: newValue)
13911391
}
13921392
backingMyVar = newValue
13931393
}
@@ -1399,7 +1399,7 @@ This "broadcast a notification that the value has changed" implementation cannot
13991399

14001400
```swift
14011401
protocol Observed {
1402-
func broadcastValueChanged<T>(oldValue: T, newValue: T)
1402+
func broadcastValueWillChange<T>(newValue: T)
14031403
}
14041404

14051405
@propertyWrapper
@@ -1419,7 +1419,7 @@ public struct Observable<Value> {
14191419
get { return stored }
14201420
set {
14211421
if newValue != stored {
1422-
observed?.broadcastValueChanged(oldValue: stored, newValue: newValue)
1422+
observed?.broadcastValueWillChange(newValue: newValue)
14231423
}
14241424
stored = newValue
14251425
}
@@ -1441,27 +1441,34 @@ public class MyClass: Superclass {
14411441
}
14421442
```
14431443

1444-
This isn't as automatic as we would like, and it requires us to have a separate reference to the `self` that is stored within `Observable`.
1444+
This isn't as automatic as we would like, and it requires us to have a separate reference to the `self` that is stored within `Observable`. Moreover, it is hiding a semantic problem: the observer code that runs in the `broadcastValueWillChange(newValue:)` must not access the synthesized storage property in any way (e.g., to read the old value through `myVal` or subscribe/unsubscribe an observer via `$myVal`), because doing so will trigger a [memory exclusivity](https://swift.org/blog/swift-5-exclusivity/) violation (because we are calling `broadcastValueWillChange(newValue:)` from within the a setter for the same synthesized storage property).
14451445

1446-
Instead, we could extend the ad hoc protocol used to access the storage property of a `@propertyWrapper` type a bit further. Instead of (or in addition to) a `wrappedValue` property, a property wrapper type could provide a `subscript(instanceSelf:)` and/or `subscript(typeSelf:)` that receive `self` as a parameter. For example:
1446+
To address these issues, we could extend the ad hoc protocol used to access the storage property of a `@propertyWrapper` type a bit further. Instead of a `wrappedValue` property, a property wrapper type could provide a static `subscript(instanceSelf:wrapped:storage:)`that receives `self` as a parameter, along with key paths referencing the original wrapped property and the backing storage property. For example:
14471447

14481448

14491449
```swift
14501450
@propertyWrapper
14511451
public struct Observable<Value> {
1452-
public var stored: Value
1452+
privatew var stored: Value
14531453

14541454
public init(initialValue: Value) {
14551455
self.stored = initialValue
14561456
}
14571457

1458-
public subscript<OuterSelf: Observed>(instanceSelf observed: OuterSelf) -> Value {
1459-
get { return stored }
1458+
public static subscript<OuterSelf: Observed>(
1459+
instanceSelf observed: OuterSelf,
1460+
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value>,
1461+
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
1462+
) -> Value {
1463+
get {
1464+
observed[keyPath: storageKeyPath].stored
1465+
}
14601466
set {
1461-
if newValue != stored {
1462-
observed.broadcastValueChanged(oldValue: stored, newValue: newValue)
1467+
let oldValue = observed[keyPath: storageKeyPath].stored
1468+
if newValue != oldValue {
1469+
observed.broadcastValueWillChange(newValue: newValue)
14631470
}
1464-
stored = newValue
1471+
observed[keyPath: storageKeyPath].stored = newValue
14651472
}
14661473
}
14671474
}
@@ -1474,39 +1481,35 @@ public class MyClass: Superclass {
14741481
@Observable public var myVar: Int = 17
14751482

14761483
// desugars to...
1477-
private var $myVar: Observable<Int> = Observable(initialValue: 17)
1484+
private var _myVar: Observable<Int> = Observable(initialValue: 17)
14781485
public var myVar: Int {
1479-
get { return $myVar[instanceSelf: self] }
1480-
set { $myVar[instanceSelf: self] = newValue }
1486+
get { Observable<Int>[instanceSelf: self, wrapped: \MyClass.myVar, storage: \MyClass._myVar] }
1487+
set { Observable<Int>[instanceSelf: self, wrapped: \MyClass.myVar, storage: \MyClass._myVar] = newValue }
14811488
}
14821489
}
14831490
```
14841491

1485-
This change is backward-compatible with the rest of the proposal. Property wrapper types could provide any (non-empty) subset of the three ways to access the underlying value:
1492+
The design uses a `static` subscript and provides key paths to both the original property declaration (`wrapped`) and the synthesized storage property (`storage`). A call to the static subscript's getter or setter does not itself constitute an access to the synthesized storage property, allowing us to address the memory exclusivity violation from the early implementation. The subscript's implementation is given the means to access the synthesized storage property (via the enclosing `self` instance and `storage` key path). In our `Observable` property wrapper, the static subscript setter performs two distinct accesses to the synthesized storage property via `observed[keyPath: storageKeyPath]`:
1493+
1494+
1. The read of the old value
1495+
2. A write of the new value
1496+
1497+
In between these operations is the broadcast operation to any observers. Those observers are permitted to read the old value, unsubscribe themselves from observation, etc., because at the time of the `broadcastValueWillChange(newValue:)` call there is no existing access to the synthesized storage property.
14861498

1487-
* For instance properties, `subscript(instanceSelf:)` as shown above.
1488-
* For static or class properties, `subscript(typeSelf:)`, similar to the above but accepting a metatype parameter.
1489-
* For global/local properties, or when the appropriate `subscript` mentioned above isn't provided by the wrapper type, the `wrappedValue` property would be used.
1499+
There is a secondary benefit to providing the key paths, because it allows the property wrapper type to reason about its different instances based on the identity of the `wrapped` key path.
14901500

1491-
The main challenge with this design is that it doesn't directly work when the enclosing type is a value type and the property is settable. In such cases, the parameter to the subscript would get a copy of the entire enclosing value, which would not allow mutation, On the other hand, one could try to pass `self` as `inout`, e.g.,
1501+
This extension is backward-compatible with the rest of the proposal. Property wrapper types could opt in to this behavior by providing a `static subscript(instanceSelf:wrapped:storage:)`, which would be used in cases where the property wrapper is being applied to an instance property of a class. If such a property wrapper type is applied to a property that is not an instance property of a class, or for any property wrapper types that don't have such a static subscript, the existing `wrappedValue` could be used. One could even allow `wrappedValue` to be specified to be unavailable within property wrapper types that have the static subscript, ensuring that such property wrapper types could only be applied to instance properties of a class:
14921502

14931503
```swift
1494-
public struct MyStruct {
1495-
@Observable public var myVar: Int = 17
1496-
1497-
// desugars to...
1498-
private var $myVar: Observable<Int> = Observable(initialValue: 17)
1499-
public var myVar: Int {
1500-
get { return $myVar[instanceSelf: self] }
1501-
set { $myVar[instanceSelf: &self] = newValue }
1502-
}
1504+
@availability(*, unavailable)
1505+
var wrappedValue: Value {
1506+
get { fatalError("only works on instance properties of classes") }
1507+
set { fatalError("only works on instance properties of classes") }
15031508
}
15041509
```
15051510

1506-
There are a few issues here: first, subscripts don't allow `inout` parameters in the first place, so we would have to figure out how to implement support for such a feature. Second, passing `self` as `inout` while performing access to the property `self.myVar` violates Swift's exclusivity rules ([generalized accessors](https://github.com/apple/swift/blob/master/docs/OwnershipManifesto.md#generalized-accessors) might help address this). Third, property wrapper types that want to support `subscript(instanceSelf:)` for both value and reference types would have to overload on `inout` or would have to have a different subscript name (e.g., `subscript(mutatingInstanceSelf:)`).
1507-
1508-
So, while we feel that support for accessing the enclosing type's `self` is useful and as future direction, and this proposal could be extended to accommodate it, the open design questions are significant enough that we do not want to tackle them all in a single proposal.
1509-
1511+
The same model could be extended to static properties of types (passing the metatype instance for the enclosing `self`) as well as global and local properties (no enclsoing `self`), although we would also need to extend key path support to static, global, and local properties to do so.
1512+
15101513
### Delegating to an existing property
15111514

15121515
When specifying a wrapper for a property, the synthesized storage property is implicitly created. However, it is possible that there already exists a property that can provide the storage. One could provide a form of property delegation that creates the getter/setter to forward to an existing property, e.g.:

0 commit comments

Comments
 (0)