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

We should produce a better error message if there's a throwing init(initialValue:). We shouldn't try to support throwing init(initialValue:) initializers, because code like this:

@MyWrapper var foo: Int
// something something
foo = 17

can either be $foo.value = 17 or $foo = .init(initialValue: 17), due to the rules of definition initialization.

Doug

Cold this change in the future if throwing accessors are added the language?

Sure. If throwing accessors are added to the language, we would want to revisit this. It should be a pure extension at that point (making previously ill-formed code well-formed) rather than a source-breaking change.

Doug

4 Likes

I don't remember if this has been brought up before, but when a model with a wrapped property is encoded with the synthesized encoding method, is it expected to give an output like this:

{"$i":{"value":42}}

Instead of what you would get without the property wrapper:

{"i":42}

Should property wrappers work with class inheritance?

@propertyDelegate
class A<T> {
    let value: T
    
    init(initialValue value: T) {
        self.value = value
    }
}

@propertyDelegate
class B: A<Double> { // error: Property delegate type 'B' does not contain a non-static property named 'value'

}

if I remove attribute from class B:

@propertyDelegate
class A<T> {
    let value: T
    
    init(initialValue value: T) {
        self.value = value
    }
}

class B: A<Double> {

}

@B var b = 100 // error: Class 'B' cannot be used as an attribute

@Douglas_Gregor shouldn't we ban retroactive extensions of wrapperValue from property wrapper types?

This does not work as I potentially would expect it to work, but it also compiles. Is this intended?

// Xcode 11 build so I'm using `delegateValue`
import SwiftUI

extension Binding {
  var delegateValue: String {
    "Swift"
  }
}

struct Foo {
  @Binding<Int>(getValue: { 42 }, setValue: { _ in })
  var property: Int
}

let foo = Foo()

// prints:
// Binding<Int> Binding<Int>(transaction: SwiftUI.Transaction(plist: []), _location: SwiftUI.LocationBox<SwiftUI.FunctionalLocation<Swift.Int>>, _value: 42)
print(type(of: foo.$property), foo.$property)

Hello, new member to the forums here :wave:t2:. I would like to say that watching this pitch develop (with little understanding of why it was needed) was amazing once the SwiftUI framework dropped and it all clicked into place :smile:. Thank you for renaming this to property wrappers @Douglas_Gregor as I was finding the property delegates name too similar to the other use of delegates within AppKit/UIKit.

Now that this is called property wrappers, I was wondering about how the synthesised property name could read better in use. As it is now, $foo appears the same as $0, $1 etc in closures, but it follows different semantics in what it returns. Right now we unwrap an optional by appending ! to it's name. Could the synthesised name look similar by being foo$ instead of $foo so that accessing an unwrapped value follows similar syntax?

Other than this I thought I'd add that personally I'd prefer to see the attribute syntax have a preference for lowercase. For example @lazy var foo so that it matches with private var func @objc etc etc. I saw mention of @builder(HTML) elsewhere which I like as it also looks similar to #selector(myFunc).

Thanks for reading :slightly_smiling_face:

Edit: Thinking about my first suggestion a little bit more, I think I'd quite like to see foo@ instead of foo$ as this would make it pretty clear that it's accessing the underlying storage created by an attribute.

4 Likes

Welcome to the forums! :)

After a little experimenting with this feature in SwiftUI, I was thinking something similar. It feels quite arbitrary that accessing the storage of an @Wrapper property is done via the $ syntax. It would feel more natural to me to reuse the @ character, since in the declaration the @ is attached to the actual type we get from accessing the underlying storage. (Or, what if property wrappers were declared as $Wrapper var num: Int? We'd lose the parallel with compiler attributes, but would it be so bad if user-defined attributes used a different character?)

It's intentional that we ignore it if it's defined outside of the property type definition. I suppose we could warn about it being ignored.

Doug

2 Likes

I think that would be the optimal solution here.

Welcome!

The reason for $foo is that it's already a part of the identifier grammar, and these synthesized properties are no different that any other properties. Something like foo@ is more operator-like, because prefix @ is already reserved for introducing attributes (which have their own grammar). It's also technically a breaking change to the grammar to parse foo@ as an existing name, because currently the @ will be taken to introduce a new attribute.

I think it would be bad if user-defined attributes used a different character, because (1) then they're not really "attributes", and (2) that cuts off a future path where some existing compiler-provided attributes become library-provided.

Custom attributes like the ones defined by property wrappers refer to types, and the Swift API Design Guidelines state that those should be types. That doesn't prevent someone from naming a type with an initial lowercase to get the effect you're asking for, but I prefer the uppercase to distinguish custom/library-centric attributes from compiler built-in attributes.

A Different Doug

(EDIT: Clarify that this use of foo@ would be source-breaking and
note why I don't want to use $ for custom attributes)

4 Likes

Fair enough, I definitely see the value in unifying the two concepts. I also think, though, that there's something to be said for differentiating between "this is going to invoke some sort compiler magic" and "this is just using a type that you could write yourself, if you wanted to." "Attributes" per se only exist as compiler features currently, so introducing user-defined attributes with a different character are really attributes as long as we say they are (though we're descending into a semantic debate at that point...). It also wouldn't preclude the "long" version of that path, i.e. deprecation, fix-it, eventual removal.

Following this convention would also cut off the future path you mention. Are you imagining that we would just define these "converted" attributes as lowercased types as a compromise for source compatibility?

1 Like

:smile:

Awww I thought this was a neat idea when I read it, thinking $Thing clearly says it's a property wrapper and not an attribute, but given (2) that clearly couldn't happen.

Ok, this makes more sense knowing that (eventually) all attributes beginning with upper case will actually be property wrappers and not compiler attributes :+1:t2:

Sorry! My understanding of source compatible is probably a bit naive :pleading_face: Thanks for being patient and discussing anyway :upside_down_face:

The Other Doug

1 Like

i've been experimenting with @_propertyWrapper more and more, and one thing really sticks out and it's having to spell out

.init(initialValue: foo)

does "init" really need to be repeated here? i like to do all my initialization in the init() method and spelling out .init(initialValue:) wastes a lot of horizontal space

init() 
{
    self.$model = .init(initialValue: .init(c: .init(a: 1, b: 2), d: 3))
}

we should really shorten that to just init(value:) and it would be just as clear, but why can't we do the obvious thing and allow

init() 
{
    self.model = .init(c: .init(a: 1, b: 2), d: 3)
}

? in the general case, setting a computed property is equivalent to calling a method on self, but property wrappers aren't so general...

also, i don't understand why this type inference doesn't work:

@_propertyWrapper 
struct State<Value> 
{
    @_propertyWrapper 
    final 
    class Binding
    {
        @State 
        var value:Value 
        
        init(initialValue:Value) 
        {
            self.value = initialValue
        }
    }
    
    var sequence:UInt = 0
    var value:Value 
    {
        didSet 
        {
            self.sequence &+= 1
        }
    }
    
    init(initialValue:Value) 
    {
        self.value = initialValue
    }
}
struct Main 
{
    @State.Binding // needs @State<Model>.Binding
    var model:Model
}
error: property wrapper type 'State.Binding' must either specify all generic arguments or require only a single generic argument
    @State.Binding 
  1. The property wrapper type must have a property named value

I've been playing with property wrappers and re-implementing @Binding and @State from SwiftUI, and just realised that the name value might not be the best choice here when used in conjunction with @dynamicMemberLookup like it is in the context of SwiftUI

Especially, in SwiftUI @Binding makes use of @dynamicMemberLookup to easily map from a Binding to another Binding to an inner property:

let personBinding: Binding<Person> = ...
let nameBinding: Binding<String> = personBinding.name

The problem is that if we have a model object with a property exactly named value, the behavior will be different for this value property as opposed to all other properties, because .value is a property already existing on Binding so it won't go thru the subscript(dynamicMember:) like all others.

This is normal behavior and by design of @dynamicMemberLookup, but in the context using Property Wrappers with DML (like SwiftUI heavily does), given that value seems like a pretty common name for a property, we are quite likely to clash and hit this inconsistency in behavior, which could quickly become confusing – especially to new users of SwiftUI who don't really know the implementation details of @State and @Binding


For example, suppose we have these definitions:

struct CartItem {
    var name: String
    var value: Double
}

struct CartCell: View {
    @State var item: CartItem
    var body: some View {
        HStack {
            Text(item.name)
            Text("\(item.value)")
        }
    }
}

Then it's easy to encounter the confusing behavior here:

let cell = CartCell(item: CartItem(name: "Mac Pro", value: 5999.00))

let itemBinding: Binding<CartItem> = cell.$item // OK
let nameBinding: Binding<String> = cell.$item.name // OK
let valueBinding: Binding<Double> = cell.$item.value // cannot convert value of type 'CartItem' to specified type 'Binding<Double>'

Here cell.$item.value will access the Binding<CartItem>.value here, thus returning the CartItem pointed by the property wrapper's value, as opposed to transforming the Binding<CartItem> into a Binding<Double> similar to how it did for name.


By design of @dynamicMemberLookup, the problem will always exist whatever name we chose for the value and wrapperValue properties, but value seems like a quite common name for a property making it way too likely imho to run into this inconsistency.

I propose we thus find a less common name for the value property in this proposal. My suggestion would be propertyWrapperValue instead of just value, and propertyWrapper instead of wrapperValue – making those names way less likely to be encountered in a custom Model object of the user's code**.

3 Likes

I see you concerns and it makes sense that @dynamicMemberLookup is likely to collide with some of the members from the annotated type. However I don't like the name changes you suggested. Both propertyWrapperValue and propertyWrapper are either too verbose or/and ambiguous in their meanings. To me as a non native English speaker both read as they providing the same thing.

Instead I propose only to change value into wrappedValue.

This plays nicely with wrapperValue. One of them refers to the value the property wrapper wraps and the other what the property wrapper itself will become if you try to access it.

Furthermore I understand that the difference between them is just a single character, but I also think that this does not really matter as wrapperValue cannot be accessed directly anyway. So both wrappedValue and wrapperValue won't possibly collide with anything unlike value did in the above example.

@Douglas_Gregor what do you think about wrappedValue?


@AliSoftware meanwhile you can workaround your issue by writing cell.$item[\.value].

12 Likes

I'll give a +1 for wrappedValue. The point of this feature is that you wouldn't be referring to the wrapper's property by name, except from within the implementation of the wrapper. The few exceptions that come up can deal with with a longer property name.

[Edit] Technically, wrappedValue would be referring to the property using the wrapper, but I think we can get past that.

9 Likes

The wrappedValue has so precise meaning. Definitely +1

7 Likes

On seeing this in action in SwiftUI, I am a little concerned with how magical it seems.

If I take

var password: String = ""

and add @Published before it per the code examples, I get an observable to use with $

@Published var password: String = ""

let sub = $password.sink { 
    print($0)
}

I think we've added a layer of indirection and magic to the language; we should just be explicitly creating a publisher instead. I imagine this pattern will be everywhere in Swift :/

2 Likes

It is explicit, though. It also reflects that the behaviour (defined by the wrapper's type) isn't the important type. The important type is the wrapped one.

2 Likes