SE-0258: Property Wrappers (second review)

I think I like the idea of using & but maybe for the backing storage instead of the binding.

If I pass a value as inout that has a backing storage, I think it makes sense that &someProp refers to the backing storage. What else would it be passing if not the backing storage? So it this way if people want the backing storage, they are going to need to get it as an inout which should help with exclusivity.

1 Like

Apologies for not being precise here, but I think the net effect still stands? The library author (of Widget) might get a hint that something's changed because _foo.reset() is no longer valid, but they'll still have their public API changed to include wrapperType'd $foo and $bar? Maybe it's somewhat disingenuous to say defining wrapperType dictates access level, but it still dictates how external users see the wrapped property (or functionality it provides). By first not implementing wrapperValue, then doing so later, the property wrapper author is still affecting their consumers in a way I see no precedence of.

I think this use case falls into the "not for you (yet)" camp I mentioned. I think this should require a way for you to specify access levels yourself which isn't part of this proposal.


FWIW, I see the value of wrapperValue (or whatever it ends up being called), but think it's so closely related the the backing store (they're both providing access to stuff defined on the property wrapper) that making its access level anything other than exactly the same as the backing store is counter intuitive.

1 Like

Touché. I don't have a good solution for that, but I figured most of property wrappers will be of 2 types; either modifying the getter/setter behaviour, or venting out the wrapperValue, so adding/removing it would be unlikely.

We could consider adding/removing wrapperValue to be non-backward-compatible change.

1 Like

That does not scale pass the module boundary from my point of view. If the library author chooses not to expose the property wrapper type used to wrap some property and this property is get only publicly then there is no $foo for the client, while the current wrapperValue triggers the compiler to create a property that can later be exposed to clients while keeping the @WrapperType annotation and the wrapper type itself a secret.

I understand your desire of having a fixed meaning of $ prefixed values, but this does not fully cover the story around wrapperValue functionality. It rather removes a proposed feature and adds more issues which really shouldn't exist to the property wrapper types.

I’m mostly in agreement with @Nevin regarding the state of wrapperValue, and agree that it should at the very least be part of a follow up proposal, but this mutable reference syntax is necessary with the constraints that have been laid down by the Core Team, since using $foo to create a Binding in a SwiftUI context is non-negotiable. Given that, I would much prefer a solution which folds the $ syntax into a consistent and clear language construct rather than an arbitrary user-defined API which is completely inscrutable at point-of-use.

4 Likes

I accidentally called property wrappers as property builders. That got me thinking, what are the main differences from a user facing API standpoint of view between @funcWrapper and Function Builders.

What we are missing is people being able to create their own attributes.

Can you consider making backing storage fileprivate instead of private. I miss the simplicity of early Swift access control.

3 Likes

I think we should seriously consider this direction.

One question: With $foo just meaning a mutable reference, how would you expose things like observe() on an @Observable to outside of the type. I don't think it necessarily needs to have a $, but we do need some way of exposing desired functions like observe() without manually providing it for each observable property (since that would erase the benefit of an @Observable wrapper)

2 Likes

One possibility is using an infix @ similarly to how people were discussing @Chris_Lattner3's third step earlier:

myVar.myObservableProp @ observe {...} //I'd also be ok with myObservableProp.@observe {...}

The nice thing about this is it mirrors the public declaration of the feature:

struct MyType {
    @Observable var myObservableProp : Int
}

If we go this route, I would recommend using a second attribute (instead of access control) to mark the properties which are exposed in this way:

@ValueWrapper
struct Observable {
    @WrapperFunc func observe(...) {...)  //Someone probably has a better name for this
}

Public properties and functions not annotated in this way would be available on _foo.

That leaves $foo as @Nevin's (and SwiftUI's) concept of a mutable reference.

1 Like

Actually, now that I think about it, I kind of prefer:

myVar.myObservableProp.@observe {...}

Everything else would be the same as I wrote above...

1 Like

Bike-shedding: wrapperValue -> associatedValue.

Since, generally speaking, it doesn't really wrap any of the value, but provide a functionality closely related to wrappedValue.

Also, it seems to me that wrapperValue looks like associatedType for a protocol, used when there's another value closely associated with it, but is more or less independent from the property wrapper itself.

This is very similar to the associated object capability of Objective-C, so I'm not sure it's a very good alternative.

Afair, the original concept took the syntax from Kotlin - and imho this option has been discarded too quickly:

  • It's a tested design
  • It has no ambiguities with nesting / order of wrappers
  • It has an obvious answer for access control, which has become a big source for controversy (think public var test: Int by public Lazy { 55 * 44 }

Also, I don't think the existing use of "@" is a argument in favor of the current spelling at all:
Property wrappers won't be the only thing that makes use of this syntax, will it?
So we'll have several completely different things which are expressed in the same way, and I don't think this is any better than having a dedicated way to specify wrappers.

This thread is incredibly long and I haven't followed the preceding discussions closely so apologies if this has already been discussed, but has a function-call-like syntax been discussed for accessing the wrapper and backing storage? Something like this:

 @MyWrapper var foo: Int = ...
 #storage(foo)
 #wrapper(foo)

It's slightly more verbose, but with the benefit clarity and explicitness. It seems arbitrary to use the $ character for this purpose.

1 Like

It's clear and intuitive if you know that foo is backed by a MyWrapper, but what if you apply these operators to some other Int? I suppose that could be made to fail at compile time, else it would sort of have to return an optional, which wouldn't be nice.

Philosophically I don't like the idea of distinguishing between different Int's based on their attribute, or lack thereof. That would introduce a new category in the type system, which would have knock on effects.

In any case the "wrapper" will have to be accessed with $foo, that seems to be a pretty hard requirement since that has been abundantly communicated already in connection with SwiftUI.

FYI, this was fixed by [SE-0258] Clean up memberwise initializer / default-initialization interaction by DougGregor · Pull Request #25386 · apple/swift · GitHub

I would just like to voice, after following the thread for days, that I hope ‘wrapperValue’ and ‘wrappedValue’ are called something more distinct.
Every time I had to stop and read twice to understand the posts here. Maybe is just me but the visual similarity of otherwise different concepts concerns me.

private better matches existing conventions, so no.

Doug

"Dictate the API" makes it sound like a property wrapper type can arbitrarily mutate the API of a client. That's clearly not the case here---it's exposing another property $foo that has additional API. @John_McCall noted that are really two kinds of property wrapper types: those that are a detail of the implementation of the property itself (@Lazy is like this) and those for which the presence of the wrapper is fundamental to understanding the property itself (things like @tanner0101's @Field or SwiftUI's @State/@Binding). For the latter case, exposing the wrapper/projection API via $foo is important.

I disagree with this assessment. When you declare a property

@A var foo: String

The property you declare is "wrapped" in an A that provides the storage, and foo retrieves the value from that wrapper.

You didn't elaborate here, but I suspect you're referring to something like the delegating to an existing property future direction.

FWIW, both of these are noted as my intended changes for revision #vapor-3

More control over the access of the backing storage property is covered as a future direction.

For reference, we did explore the notion of a "mutable reference" fairly deeply, both based on the notion of overloading & and as a separate construct to try to abstract the notion of a mutable binding to something. It didn't pan out as a good general-purpose language feature. Key path member lookup and wrapperValue (as part of property wrappers) turned out to be more generally usable language features than what we were designing.

Is it possible that there is some extensible design for mutable references that addresses all of the concerns voiced here? Perhaps---it's impossible to prove that no good design exists based only on the fact that we tried and didn't find one---but potential future designs have a habit of looking like silver bullets until we try to make them a reality.

Doug

9 Likes

I think this response misses the point I was trying to make. The dictate doesn't come from the $foo being added to the consuming type, but the fact that its access level is made to be the same as the property it's on.

If we look at LongTermStorage<Value> on the proposal, it's implemented using a wrapperValue of UnsafeMutablePointer<Value>. In my mind, this fact is telling users "if you want to use LongTermStorage<Value> you must also expose an UnsafeMutablePointer<Value>" the "must" here is the dictate. I don't think the author of LongTermStorage<Value> intended for this to happen, infact my reading is quite the contrary: they wanted to hide UnsafeMutablePointer<Value>, providing safe access through a property that looks just like a Value.

I've not been able to seriously use SwiftUI, but from all the examples I've seen, the only thing that needs to know that properties are being wrapped are the immediately consuming types (i.e. those that have private access anyway). From what I understand of SwiftUI, it'd be very odd to bind to another object's @Binding or @ObjectBinding projection directly – the patten being that bindings are passed down as needed to views at construction.

I don't necessarily agree that there are any property wrappers that only make sense if their projection is available as well. There are certainly some that seem this way, but I don't think it's Swift's, or property wrapper author's, place to mandate this. There's some small short-term pain to just expose these manually, and a mid-term smaller pain to explicitly define their access level once that features's added. In both cases this is when the user of the property wrapper decides, not the author of the property wrapper.

If we truly believe some wrappers/projections should be available alongside properties themselves, could we not use wrapperValue as the proxy? There are reasons to use wrapperValue (e.g. vending a library type instead of the property wrapper type) which do not imply the projection should be available as well. Something like @exposedPropertyWrapper?

4 Likes