SE-0258: Property Wrappers (second review)

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

I am super excited to see this nearing completion, as well as the seamless integration it has with flagship features like SwiftUI. :slight_smile:

Apologies if this is the wrong place for something like this, but I think it's hugely important to the initial release of this feature to also have it seamlessly integrated with the Codable API. One of the biggest complaints about Codable is how verbose it can be, which sounds like a perfect use-case for a modern declarative API. JSON decoding happens to be one of the most common spots for beginners to run into problems, and often frustration, with Swift's strict type system.

A simple API like: @JSON(key: String) would go a long way, and then extra decoding/encoding functions can be added to the containers, eg:

mutating func decode(_ type: JSON<UInt32.Type>) throws -> UInt32

I agree that Codable is an important use case for property wrappers. Last I checked (Xcode 11 beta 1), the default automatically synthesized coding key for a wrapped property is $foo. I think this should be changed to just foo before the release so that we can write wrappers to provide custom encoding utilities.

3 Likes

This has already been fixed in the compiler to align with the proposal under review, but the changes didn't make it into Xcode 11 beta 1 or 2.

Doug

3 Likes

The second point is missing here. What was it that you wanted to say?


Is there a reason for calling it projectionValue instead of projectedValue?

You're right, I liked projectedValue better for the consistent "-ed".

Doug

1 Like

@Douglas_Gregor what is your opinion as a proposal author about a name and attribute adjustment to leave room for future subscript and func wrappers?

A few names were mentioned upthread:

  • @varWrapper, @subscriptWrapper, @funcWrapper
  • @wrapper(var), @wrapper(subscript), @wrapper(func)

A few names were mentioned upthread:

  • @varWrapper, @subscriptWrapper, @funcWrapper
  • @wrapper(var), @wrapper(subscript), @wrapper(func)

I maybe late to this game but my opinion is we should use the Property Wrapper as a name since it will go along with the existing Property Observer feature.

1 Like

Honestly, I dislike them all. "Property" is the general term for these declarations, and I feel like "property wrappers" captures what this feature is all about. When I've explained it to people with this nomenclature, it's "clicked" better than the other terminology and it feels more like a coherent feature. I also don't feel we have sufficient use cases for applying this to non-properties to leap to any generalized naming scheme at this point.

Doug

3 Likes

I really appreciate your hard work on the feature and how it has shaped over the past month.

There is one other thing that I would like to know and probably the community as well.
Right now property wrappers are supported for stored properties only. Do you think we still could ship support for local properties in Swift 5.1?? I personally have tons of ideas how I would wrap local variables to make repeating behaviors robust and tested.

1 Like

Could you name some example use cases? For sure a lot of property wrapper types don’t make sense at all locally, but which ones do?

For example before toggle on Bool was introduced you had to write these very long paths yourself to get the functionality you wanted (ignore the possibility of an extension a moment). Today you might also copy some state, mutate it and copy it back to safe yourself some typing. Well you can improve that with a property wrapper type that operates with key paths.

@propertyWrapper
struct MutableRefence<Root, Value> {
  init(root: Root, keyPath: WritableKeyPath<Root, Value>) { ... }
  var wrappedValue: Value { /* accessors via key path */ }
}

@MutableReference(root: self, keyPath: \.something.foo.baz)
var baz: Value

baz.mutatingFunc() // will mutate the referenced storage

Another example is clamping of values for local computation.

I originally would like to throw valueProjection into the mix as well since it’s a projection of the storage, but now I like projectedValue better.

I doubt it. lazy has been broken in local contexts forever, and it'll take a bit more work to detangle these issues for both lazy and property wrappers.

Doug

Why is it necessary to fix lazy before property wrappers? Isn‘t this just invisible synthesis of code? I mean I can write it myself but that‘s a lot of boilerplate.