Skip to content

Commit

Permalink
Apply suggestions from review
Browse files Browse the repository at this point in the history
Co-authored-by: Xiaodi Wu <13952+xwu@users.noreply.github.com>
  • Loading branch information
2 people authored and Amritpan Kaur committed May 30, 2023
1 parent 60f7f59 commit e0b7922
Showing 1 changed file with 38 additions and 33 deletions.
71 changes: 38 additions & 33 deletions proposals/NNNN-allow-let-property-wrapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

Allowing property wrappers to be applied to `let` properties improves Swift language consistency and expressivity.

Today, a property wrapper can be applied to a property of any type but the rules for declaring these wrapped properties are varied and singular to property wrappers. For example, a struct instance without the property wrapper attribute can be declared with either a `var` or a `let` property. However, mark the struct as a property wrapper type and the compiler no longer allows it to be written as a `let` wrapped property:
Today, a property wrapper cannot be applied to a property declared with `let`:

```swift
@propertyWrapper
struct Wrapper {
Expand All @@ -26,14 +27,17 @@ struct S {
@Wrapper let value: Int // error: property wrapper can only be applied to a ‘var’
}
```
Permitting wrapped properties to mimic the rules for other type instances that can be written with either a `var` or a `let` will simplify the Swift language. Additionally, `let` wrapped properties would expand property wrapper usage to allow `nonisolated` wrapped properties and simplify property wrappers usage where the backing type is a reference type.

Removing this limitation would allow `nonisolated` wrapped properties (which must be declared with `let`) and simplify property wrapper usage where the backing type is a reference type.



## Proposed solution

We propose to allow the application of property wrappers to let declared properties, which will permit the wrapper type to be initialized only once without affecting the implementation of the backing storage.
We propose to allow the application of property wrappers to `let` declared properties, which will permit the wrapper type to be initialized only once without affecting the implementation of the backing storage.

For example, consider the following implementation for a `@BoilingPoint` property wrapper:

For example, the following is the implementation for a `@BoilingPoint` property wrapper:
```swift
@propertyWrapper
struct BoilingPoint<T> {
Expand All @@ -47,59 +51,57 @@ struct BoilingPoint<T> {
func toFarenheit() {}
}

struct Temperature {
@BoilingPoint var water: Double = 373.1
}
```

The initial value for `water` is set using the `init(wrappedValue:)` initializer of the `BoilingPoint` property wrapper type. To ensure that `water` is not changed again, we could add logic to the `wrappedValue` setter to check if the property was already initialized and prevent re-assignment. However, this is an inconvenient solution.
The initial value for `water` is set using the `init(wrappedValue:)` initializer of the `BoilingPoint` property wrapper type. To ensure that `water` is not changed again, we could add logic to the `wrappedValue` setter to check if the property was already initialized and prevent re-assignment. However, this is an inconvenient solution that involves modification of the property wrapper implementation.

Instead, we declare the `water` property with `let`. The compiler synthesizes a local `let` declaration for the backing storage property, passing the assigned value to `init(wrappedValue:)`:

Instead, we could declare the `water` property with a `let`, synthesize a local `let` constant for the backing storage property (prefixed with an underscore), and only allow the property wrapper to be initialized once, passing the assigned value to `init(wrappedValue:)`. Now, rewriting `Temperature`’s properties as `let` constants will translate to:
```swift
struct Temperature {
@BoilingPoint let water: Double = 373.1

// ... introduces _water stored property
}
// ...translates to:
struct Temperature {
private let _water: BoilingPoint<Double> = BoilingPoint<Double>(wrappedValue: 373.1)

// ... getter-only computed property
// getter-only computed properties:
@BoilingPoint var water: Double {
get { return self._water.wrappedValue }
}

// ... getter-only projectedValue property
internal var $water: Double {
var $water: Double {
get { return self._water.projectedValue }
}
}
```

Declaring a `let` wrapped property of `water` of type `BoilingPoint` generates a `let` declared storage property of `_water`, a getter-only computed property that assigns the declared value to the backing property wrapper's `wrappedValue`. A let declared wrapped property also generates `$water`, a getter-only projectedValue property.

The `let` declared property can also be initialized once via the generated storage property. For example, reusing the above example:
A `let` declared property can be initialized once via the generated storage property:

```swift
struct Temperature {
@BoilingPoint let water: Double

init() {
_water = BoilingPoint(wrappedValue: 373.1)
}
}
```

After the `let` declared property has been initialized, attempting to reassign it will show an error. Clients can continue to manipulate the projection just like with `var` declared properties.
After the `let` declared property has been initialized, attempting to reassign it will be an error. Users can continue to manipulate the projection just as with `var` declared properties:

```swift
temperature.water = 400.0 // error: cannot assign to property: 'water' is a 'let' constant
temperature.$water.toFarenheit() // converts to 212.0
temperature.water = 400.0 // error: cannot assign to property: 'water' is a 'let' constant
temperature.$water.toFarenheit() // converts to 212.0
```

Property wrappers with `let` declarations will be allowed both as members and local declarations, as envisioned by [SE-0258](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md) for `var` declared property wrappers. All other property wrapper traits also remain unchanged from SE-0258.
Property wrappers with `let` declarations will be allowed both as members and local declarations, as envisioned by [SE-0258](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md) for `var` declared property wrappers.

## Detailed Design

Marking a property wrapped declaration with a `let` makes that property a getter-only computed property and introduces a `let` synthesized stored property whose type is the wrapper type. This expands where and how property wrappers can be used in Swift.

### Nonisolated property wrappers

Actor methods and properties, which are inherently isolated to that actor, can be marked `nonisolated` to allow them to be accessed from outside their actor context. At this time, the `nonisolated` keyword cannot be used on property wrappers because `var` declared property wrappers generates a `var` declared backing property that is not safe from race conditions and so cannot be `nonisolated`.

```swift
@propertyWrapper
struct Wrapper {
Expand All @@ -113,7 +115,8 @@ class C {
}
```

Let declared property wrappers will now allow us to write nonisolated property wrappers as the backing property will also be stored `let` property.
This proposal will allow us to write nonisolated property wrapper, as the backing property will be a stored `let` property:

```swift
@MainActor
class C {
Expand All @@ -128,7 +131,8 @@ class C {

### Property wrappers with backing reference types

A `let` wrapped property could be useful for reference types like a property wrapper class. Typically property wrappers are written for value types but occasionally a protocol like `NSObject` may require the use of a class. For example:
A `let` wrapped property could be useful for reference types like a property wrapper class. Typically property wrappers are value types, but occasionally a protocol like `NSObject` may require the use of a class. For example:

```swift
@propertyWrapper
class WrapperClass<T> : NSObject {
Expand All @@ -147,12 +151,13 @@ class C {
}
}
```
Even though `value` is declared with a class type property wrapper, it can only be assigned once, preventing any future unintentional changes to the property wrapper class instance in this context.
Even though `value` is declared with a class type property wrapper, the synthesized computer property '\_value' is a getter-only property and can only be assigned once. Let declaring a wrapped property wrapper with a backing class type prevents any future unintentional changes to the property wrapper class instance in this context.


### SwiftUI

SwiftUI property wrappers may also benefit from a let declaration. For example, `@ScaledMetric` in its simplest usage can be written with a `let`:

```swift
struct ContentView: View {
@ScaledMetric let imageSize = 10
Expand All @@ -173,11 +178,11 @@ Similarly, other SwiftUI property wrappers could be `let` declared when they do

Currently, SwiftUI offers two property wrappers, [`@State`](https://developer.apple.com/documentation/swiftui/state/wrappedvalue) and [`@Binding`](https://developer.apple.com/documentation/swiftui/binding/wrappedvalue), that have a `wrappedValue` property with a `nonmutating set`. This allows the backing types to preserve the reference semantics of the `wrappedValue` implementation.

A `var` declared `@State` property currently generates a nonmutating set that reflects `@State`'s reference based storage:
A `var` declared `@State` property currently generates a nonmutating set:

```swift
@State var weekday: String = "Monday"

// ...expands to:
private var _weekday: State<String> = State<String>(wrappedValue: "Monday")
var weekday : String {
get {
Expand All @@ -196,7 +201,7 @@ Declaring the `@State` property with a `let` will remove the nonmutating setter:

```swift
@State let weekday: String = "Monday"

// ...expands to:
private let _weekday: State<String> = State<String>(wrappedValue: "Monday")
var weekday : String {
get {
Expand All @@ -205,9 +210,9 @@ var weekday : String {
}
```

[There is an argument to be made](https://forums.swift.org/t/pitch-allow-property-wrappers-on-let-declarations/61750/20) in favor of retaining the `nonmutating set` for let declared `@State` and `@Binding` to signify their reference based storage. Afterall, a property wrapper's `wrappedValue` could be assigned via the `_weekday` backing property, even if `weekday` was marked with a `let` instead of a `var`.
[There is an argument to be made](https://forums.swift.org/t/pitch-allow-property-wrappers-on-let-declarations/61750/20) in favor of retaining the `nonmutating set` here. After all, a property wrapper's `wrappedValue` could be assigned via the `_weekday` backing property, even if `weekday` was marked with a `let` instead of a `var`.

However, we have elected to remove the `nonmutating set` for `let` declared `@State` or `@Binding` to maintain language consistency. Since property wrapper is just a struct or class with an attribute and a `wrappedValue`, a struct with a backing type of a class would not generate a nonmutating setter for its instances. For example:
However, we have elected to remove the `nonmutating set` for `let` declared `@State` or `@Binding` to maintain consistency with the behavior of unwrapped properties with backing storage of reference type. For example:

```swift
class A {
Expand All @@ -232,7 +237,7 @@ struct Test {
}
```

Should SwiftUI or general property wrapper usage evolve in a different direction in the future, this decision can be reconsidered to propagate backing type traits to property wrapper declarations.
Should property wrapper usage evolve in a different direction in the future, this decision can be reconsidered.

## Effect on ABI stability/API resilience

Expand Down

0 comments on commit e0b7922

Please sign in to comment.