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
.
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.
nonisolated
wrapped properties
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
.
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:
-
SE-0389 "Attached macros" adds "accessor macros", which also can't be applied to
let
declarations. - SE-0400 "Init accessors", under review now, adds the ability to specify the initialization behavior of a property independent of its setter.
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
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.
I have put up a new pitch to enable let declared accessor macros, and I would appreciate any feedback there.
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.