[Pitch #3] Property wrappers (formerly known as Property Delegates)

When using @CopyOnWrite var foo: T, use foo for reading $foo for read/write.

The main difference is that with @Box var foo: T, $foo itself has type Ref<T> rather than type Box<T>. Box<T> could also support dynamic member lookup, I suppose, but then $foo would be Box<T> and $foo.bar would be Ref<U>, which is... not as nice.

(I think you meant $someValue in both lines above)

I don't feel like the generics you've applied here are really changing things. The difference is at the use site---instead of having $someValue refer to the pointer that we want, you're applying the convention to use $someValue.pointer. That's possible; my view is that the design is improved by having $someValue be the pointer that we want.

Doug

The example in the pitch shows two of the parameters having the type of the property, and not the wrapper. I presume it's because those properties are known to have init(initialValue:) implementations. The first because it's initialized in the declaration, and the second because the wrapper is specified without any parameters.

If my request above, for extending the lookup logic for init(initialValue: T, ...) is implemented, would that change my Color example such that the synthesized init would accept Int parameters?

1 Like

Not as nice, but $foo.ref would be far clearer than having a type casting completely hidden at the call site and that requires to dive into the type declaration to understand what is going on under the hood.

As wrapper value would be an additive change, I think that wrapperValue can be move into "future direction" and be reconsidered once we have more experience with property wrapper.

Yes. For something like:

struct X {
  @Clamping(min: 1, max: 7) var a = 13 
}

we would get

init(a: Int = 13) {
  $a = Clamping(initialValue: a, min: 1, max: 7)
}

Doug

2 Likes

Perfect. I'm really liking how the pitch is shaping up. For what it's worth, I definitely vote for keeping wrapperValue in the initial implementation.

2 Likes

This new pitch is the best by far. I really like it.

Some minor things I see (but can make peace with):

  • Shouldn't @Swift.Lazy be Swift.@Lazy?

  • public(wrapper) is ambigious for nested wrappers.
    public fileprivate(@A) public(@B) var x is not.

  • I (still) would prefer an postfix operator $ on wrappers to access the underlying storage:

    @Lazy var foo: Int = 42
    foo$.reset(42)
    

    That way, the wrapper's storage appears more like a property of the wrapped value. Within a property wrapper, self$ could refer to the “enclosing self”.

  • Thinking about the future:

    @propertyWrapper struct Bla<Value where Value : Hashable>
    {…}
    

    limits the wrapper to hashable variables?

    Once "enclosing self" is there, will

    @propertyWrapper struct Bla : Broadcaster {…}
    

    require enclosing self to conform to Broadcaster?

    Shouldn't that be:

    @propertyWrapper<Value where Value:Hashable> : Broadcaster
    struct Bla { … }
    

I don't think so. @ introduces an attribute. That the attribute has a namespace shouldn't change the grammar.

I don't think I understand. Why would a wrapper's own protocol conformance place any demands on types which use it for their properties?

The other reason for the existing override restriction is because of observing accessors; it's surprising that this could make new storage:

override var foo: Int

but this would not (it wraps the superclass's implementation)

override var foo: Int {
  willSet { print(newValue) }
}

The restriction could be lifted in the future, and indeed property wrappers might be a sensible way to do it (@NewStorage override var foo: Int), but for now the proposal is just keeping to what's already there.

2 Likes

I still don't think wrapperValue pulls its weight, even with its use in the newly-announced SwiftUI, but I've been trying to justify it to myself by saying it's implementation-hiding for the property wrapper itself. (My main concerns are increased language complexity—most obviously around initialization—and it being more likely for the author of the wrapper to paint themselves into a corner by not exposing the wrapper type.)

I did have an implementation-side thought when talking to one of the Combine authors yesterday: does it make sense to have wrapperValue always be synthesized (as @_transparent) for a property wrapper type? That way the rule for implementing the $foo property is always consistent (except for initialization). On the other hand, it also means you can't get an offset-based key path to the property.

3 Likes

I'm going through the SwiftUI tutorials, and can someone explain to me how a value of type State<Profile> got turned into a Binding<Profile> in this example?


struct ProfileHost:View 
{
    @State var profile = Profile.default
    @State var draftProfile = Profile.default
    var body:some View 
    {
        VStack(alignment: .leading, spacing: 20) 
        {
            ...
            if self.mode?.value == .inactive 
            {
                ProfileSummary(profile: profile)
            } 
            else 
            {
                // `$draftProfile` should be the wrapper type of 
                // `self.draftProfile`, that is, a `State<Profile>`
                ProfileEditor(profile: $draftProfile)
            }
        }
        .padding()
    }
}
// ...but `ProfileEditor` is defined like this
struct ProfileEditor:View 
{
    @Binding var profile:Profile
    ...
}

the ProfileEditor.profile property has no = default value, and Binding<Value> doesn't have an init(initialValue:) initializer. so the synthesized init for ProfileEditor should look like this:

    init(profile:Binding<Profile>)

how did the State turn into a Binding?

Are you sure the type in the synthesized init is Binding<Profile> and not Binding<State<Profile>>?

(I haven't played with SwiftUI yet, so I don't actually know.)

wouldn’t that have been declared like

@Binding @State var profile:Profile

(im on linux so i have gotten to try it either im just reading the tutorial to understand how all this new stuff works)

SwiftUI’s State type uses the “wrapperValue” feature of property wrappers so that it can vend a Binding as the $ variable. The actual State instance is effectively hidden.

Doug

4 Likes

Now we can see the real motivating examples. :slight_smile:

4 Likes

And see this is a real source of confusion.

 ProfileEditor(profile: $draftProfile.binding)

is nice too and far more expressive.

4 Likes

The binding won't always be the $ variable though. If you are combining multiple wrappers, the binding variable could be $$foo, or $$$foo, and $foo would mean something else. That sounds a bit confusing.

Here's an idea: when we have multiple wrappers stacked together, we could have $ mean all of them, overloaded. Then let type inference figure out which wrapper we want. At worse we have to write ($foo as Lazy).reset() if there's some ambiguity because two wrappers are defining a reset method.

There's nothing in the proposal text about using multiple dollar signs. I interpreted that as "you have to write $foo.value if you want to access the middle wrapper". Could @Douglas_Gregor clarify how you access the nested wrapper?

There can be a pathological situation where you have two wrappers of the same type.

protocol Color { }

@propertyWrapper
class Darker: Color {
  value: Color { ... }
}

@Darker @Darker var extremelyDark: Color
print($extremelyDark as Darker) // does it print the first or second wrapper?

You should still be able disambiguate by specifying the generic parameters in full:

print($extremelyDark as Darker<Darker<Color>>)

propertyWrapper doesn't have to be generic, so there are no generic parameters to specify, example from the proposal:

@propertyWrapper
struct StringDictionary {
  var value: [String: String]
}