[Pitch] Allow Property Wrappers on Let Declarations

You asked how exactly does the transformation look like and that code snippet does exactly that :) That said, my understanding is that @amritpan is going to update the pitch to show equivalent representation of the transformation is the surface language.

Heh, that‘s a bit picky on my wording. :smirk: I am basically only interested on what the legal swift syntactic transformation looks like, not what other toolings see this like, because I as a reader am not that tool and I don‘t necessarily understand how they work. Long story short, I can only provide better feedback if I understand the pitch. :wink:

It depends on what you mean by “the same” in this context let properties that refer to reference types for example might not fit that.

No worries, I am just trying to point out the source of misunderstanding :slight_smile:

1 Like

My mistake! I did not catch the alternative let in the code sample. Let me try again.

The Swift translation for

@SmallNumber let height: Int = 1

should be

private let _height: SmallNumber = SmallNumber(wrappedValue: 1)
var height: Int {
  get { return _height.wrappedValue }
}

If we used an instance init to set a value to height instead

@SmallNumber let height: Int

then the translation will be

private let _height: SmallNumber
var height: Int {
  get { return _height.wrappedValue }
}
init() {
  _height = SmallNumber(wrappedValue: 1)
}

For SwiftUI's @State property wrapper, the let translation does not have a nonmutating set. So,

@State let weekday = "Monday"

will translate to the following and should not be able to be reassigned:

private let _weekday: State<String> = State<String>(wrappedValue: "Monday")
var weekday : String {
  get { return _weekday.wrappedValue }
}
var $weekday: Binding<String> {
  get { return _weekday.projectedValue }
}

If we wanted to mutate a wrapped property inside a ContentView body, then @State would need be attached to a var property. The following would result in an error:

struct ContentView: View {
  @State let weekday: String // change 'let' to 'var' to make it mutable
  
  init(text: String) {
    self.weekday = text
  }
  
  var body: some View {
    VStack {
      Text(weekday)
      
      Button("Weekday") {
        weekday = "Tuesday" // error: cannot assign to property: 'weekday' is a 'let' constant
      }
    }
  }
}
3 Likes

Okay it basically looks like to what I said. This starts to make way more sense now. :)
However I do disagree on the nonmutating set part. I strongly feel that it should stay as it signals that there is some reference semantics hidden there somewhere. In your original examples you do have an object as a property wrapper, which you can still reference and potentially mutate its other variables. It's the same behavior for nonmutating set, there is a reference inside the PW that is used to delagate the set operation through some kind of an object.

This possibility of the mutating in case of State is still unavoidable regardless of let, because the user can still write _weekday.wrappedValue = "Sunday" and perform a non-mutating set operation.

Furthermore I think the error message from your example should be different:

// error: cannot assign to property: 'text' is a 'let' constant

We should signal that text is a computed variable without a mutating get (aka immutable). The wrapped property isn't itself a constant and as I mentioned above is not even guaranteed to be one in certain cases like if nonmutating set is involved or if the wrappedValue's getter on the PW returns random values as it was mentioned upthread.


P.S. tiny nitpick for all SwiftUI readers. The hypothetical example in ContentView above is not recommended, as you shouldn't assign a value to State from the init unless you fully understand the outcome of this operation.

It sounds reasonable to support nonmutating set but @amritpan we might need to get creative to do that because we cannot do what local property wrappers do - synthesize a local computed variable with Introducer::Var that way VarDecl::isSettable() would produce true but isSetterMutating() would be false. Maybe it would be enough to teach ::isSettable to add a special case when VarDecl is isLet() and is wrapped at the same time. We also need to make sure that in swift interfaces (the printer you used for your examples) doesn't produce let for a computed property but always produces a var if there is a property wrapper attached to a something with Introducer::Let.

2 Likes

But what kinds of interesting behaviour would this actually allow that we can't have now?

Reference semantics if property wrapper implementation supports it.

Is that not possible today? My first question was to the OP, what is the main use-case for this feature?

Not with let properties.

Immutability of the backing storage (the property wrapper) and also the wrapper property if there is no reference semantics involved. Think about the clamped example from above. Not all the times the user wants to mutate a local variable. The desire is often to apply the effect from a PW once and that's it.

This pitch is a very welcome quality of life improvement to property wrappers!


The proposal needs some more clean ups though, especially on the explanation side of things and some of the examples. For example I cannot think of a a possible way to implement DelayedImmutable with let different than with var. In fact, that's the whole point of the "delayed" part, that it allows some mutation at a later point. For DelayedImmutable it's a single mutation, for DelayedMutable it's as many as you want. So both structures do not fit in here.

2 Likes

nonisolated wrapped properties

4 Likes

What would happen with your example if someone passed the generated Binding off to a view that mutates the state property?

struct ContentView: View {
  @State let weekday: String // change 'let' to 'var' to make it mutable
  
  init(text: String) {
    self.weekday = text
  }
  
  var body: some View {
    VStack {
      Text(weekday)
      
      TextField("Enter Weekday:", text: $weekday)
    }
  }
}

As per the discussion above it will still allow mutation as _weekday.wrappedValue has a nonmutating set which the Binding is capturing (unless it captures the internal reference mechanism, which leads to the same outcome anyways).

Binding's wrappedValue has also a nonmutating set.

The mutating in the case of State is unavoidable, regardless if the backing store is a var or a let.

1 Like

Hello @amritpan,

I had missed this proposal early on. I know I've certainly come across places where the let restriction on property wrappers is annoying, and I think it's reasonable to consider lifting the restriction.

I have two concerns with this proposal at this point, one philosophical and one procedural.

The philosophical concern is that a let constant is immutable. It can't be reassigned, yes, but if the type of the let constant is of a value type then that value cannot be changed no matter what. Applying a property wrapper to that feels like it weakens the immutability guarantee, because of the ability to introduce a backing reference type. Your WrapperClass property wrapper, for example, lets us write

_value.wrappedValue = 17

after the let has been initialized, because _value has reference semantics. It's possible that the benefit from allowing property wrappers on let's outweighs the downsides of potential mutability surprises, but we should be cognizant of the guarantees we're losing.

The procedural concern I have is that there are two other proposals in the Swift 5.9 time frame, one accepted and one under review now, that interact with this proposal:

One of the stated goals of the second proposal is to get closer to the point where macros can subsume the uses of property wrappers. That goal wasn't really understood at the time you pitched your proposal, because macros (and especially accessor macros) were really just starting to come into focus then. However, your proposal would take a step away from that goal if property wrappers could be used with let but accessor macros could not. Therefore, please consider broadening your proposal to encompass accessor macros as well, or if it doesn't work, add an Alternatives Considered section that details why it doesn't work.

I had one last thought... the init accessors proposal (SE-0400) under review allows us expression let-like single initialization behaviors for computed properties:

var onlySetOnce: Int {
  init { /* set it */ }
  get { /* get it */ }
  // no setter!
}

... which is very similar to how your proposal lets you run code on initialization, and run code when getting a value, without allowing one to directly mutate the property.

Doug

6 Likes

Thank you for this feedback! It gave me a lot to rethink with this pitch in light of the concurrent work with macros. It took me a while to catch up on understanding macros and its init accessors, but I will be re-pitching this feature as an iteration on the macro instead of the property wrapper.

2 Likes

I have put up a new pitch to enable let declared accessor macros, and I would appreciate any feedback there.

4 Likes

What is the status of the original proposal(Allow Property Wrappers on Let Declarations)? The implementation seems to be in place, but no one has ever merged the branch with the main branch. What's the problem?

Amrit very clearly provided an update in her comment right here:

The proposal has been generalized to cover accessor macros. Please continue discussion on the new pitch thread.

1 Like