[Pitch] Allow Property Wrappers on Let Declarations

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