Skip to content

Commit

Permalink
Rework proposed solution and detailed design.
Browse files Browse the repository at this point in the history
  • Loading branch information
Amritpan Kaur committed May 23, 2023
1 parent 6335616 commit fd72d7b
Showing 1 changed file with 79 additions and 40 deletions.
119 changes: 79 additions & 40 deletions proposals/0383-allow-let-property-wrapper.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Allow Property Wrappers on Let Declarations

* Proposal: [SE-038N](NNNN-filename.md)
* Proposal: [SE-NNNN](NNNN-filename.md)
* Authors: [Amritpan Kaur](https://github.com/amritpan), [Pavel Yaskevich](https://github.com/xedin)
* Review Manager: TBD
* Status: **Awaiting review**
Expand All @@ -12,7 +12,7 @@

## Motivation

Allowing property wrappers to be applied to `let` properties improves Swift language consistency and code safety.
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:
```swift
Expand All @@ -26,72 +26,108 @@ 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.
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.

Additionally, `let` wrapped properties add code safety where a user wants to expressly remove access to a property’s mutators after initializing the property once or simply does not need a mutable property wrapper. This could be useful for property wrappers that do not change or are reference types.

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

For example, [_The Swift Programming Language_](https://docs.swift.org/swift-book/LanguageGuide/Properties.html#ID617) defines a `SmallNumber` property wrapper and applies it to `UnitRectangle` properties:
For example, the following is the implementation for a `@BoilingPoint` property wrapper:
```swift
@propertyWrapper
struct SmallNumber {
private var maximum: Int
private var number: Int

var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
struct BoilingPoint<T> {
init(wrappedValue: T) {
self.wrappedValue = wrappedValue
}

init(wrappedValue: Int) {
maximum = 12
number = min(wrappedValue, maximum)
}
var wrappedValue: T
var projectedValue: T

func toFarenheit() {}
}

struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
struct Temperature {
@BoilingPoint var water: Double = 373.1
}
```
Initial values for `height` and `width` are set using the `init(wrappedValue:)` initializer of the `SmallNumber` property wrapper type. To ensure that `height` and `width` are 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.

Instead, we could declare these properties 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 `UnitRectangle`’s properties as `let` constants will translate to:
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.

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
private let _height: SmallNumber = SmallNumber(wrappedValue: 1)
var height: Int {
get { return _height.wrappedValue }
}
struct Temperature {
@BoilingPoint let water: Double = 373.1

private let _weight: SmallNumber = SmallNumber(wrappedValue: 1)
var weight: Int {
get { return _weight.wrappedValue }
// ... introduces _water stored property
private let _water: BoilingPoint<Double> = BoilingPoint<Double>(wrappedValue: 373.1)

// ... getter-only computed property
@BoilingPoint var water: Double {
get { return self._water.wrappedValue }
}
}
// ... getter/setter projectedValue property
internal var $water: Double {
get { return self._water.projectedValue }
set { self._water.projectedValue = value }
}
```
and results in code that is easy to write and understand:

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 declaration has no effect on a property wrapper's' `projectedValue`. Similar to var declared property wrappers, a let declared property generates a `projectedValue` with both a getter and setter.

The `let` declared property can also be initialized once via the generated storage property. For example, reusing the above example:
```swift
struct UnitRectangle {
@SmallNumber let height: Int = 1
@SmallNumber let width: Int = 1
}
@BoilingPoint let water: Double

init() {
_water = BoilingWater(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.
```swift
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 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. All other property wrapper traits also remain unchanged from SE-0258.

## Detailed Design

## 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 stored backing property expands where and how property wrappers can be used in Swift.

Here are three examples of how a `let` wrapped property can make current iterations more effortless.
### Nonisolated property wrapper

The `Clamping` property wrapper from [SE-0258 Property Wrappers](https://github.com/apple/swift-evolution/blob/main/proposals/0258-property-wrappers.md#clamping-a-value-within-bounds) can be rewritten to use `let` as its storage and implementation do not change, no matter its application.
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 generated a var declared backing property that could not be safe from race conditions and so cannot be `nonisolated`.
```swift
struct Color {
@Clamping(min: 0, max: 255) let purple: Int = 127
@propertyWrapper
struct Wrapper {
var wrappedValue: Int { .zero }
var projectedValue: Int { .max }
}

@MainActor
class C {
@Wrapper nonisolated var value // error: 'nonisolated' is not supported on properties with property wrappers
}
```

Let declared property wrappers will now allow us to write nonisolated property wrappers as the backing property will also be stored let property.
```swift
@MainActor
class C {
@Wrapper nonisolated let value

nonisolated func test() {
_ = value
_ = $value
}
}
```

### 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:
```swift
@propertyWrapper
Expand All @@ -113,6 +149,9 @@ class C {
```
Here, `WrapperClass` can be made an immutable property wrapper class instance, preventing any future unintentional changes to the property wrapper class type 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` instead:
```swift
struct ContentView: View {
Expand Down Expand Up @@ -193,7 +232,7 @@ struct Test {
}
```

Should SwiftUI or general property wrapper usage evolve in a different direction in the future, this decision can be reconsidered to accommodate backing type traits in instances.
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.

## Effect on ABI stability/API resilience

Expand Down

0 comments on commit fd72d7b

Please sign in to comment.