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

Ah okay so you mean it‘s not obvious if $str is actually of type CatDog or anything else, as at the point of reading it‘s not known if CatDog has a property called wrapperValue which would override the type returned by $str?! That is true and I don‘t know how to address this issue, that‘s why I didn‘t give that much feedback on that sub-feature compared to the core functionalities of property wrappers.

I realize that $ is used elsewhere in the language for synthesized properties, but for synthesized closure arguments, the types of $0, $1, etc. are immediately apparent at the point of declaration of the closure. I’m not sold that $stateVar is any better than $stateVar.binding, and it is certainly less clear at the point of use.

1 Like

Agreed. And even in the SwiftUI session, it was explained that $ indicates a binding. But that’s not really true. It only indicates a binding because the particular property wrapper in use in that code happens to expose its wrapped value as a binding.

In effect, we are saying that in swift, when you see $foo you can’t know what it actually is without fully understanding the semantics of the property wrapper. That feels... off to me.

10 Likes

I may be the weird one here but I’ve found SeiftUI usage pretty delightful. You need to learn that you can use $value.property to pass it to subviews, and you don’t really need to care about what’s going behind the scenes. I found it quite nice to work and explain.

That said I agree that in theory yes, it feels weird to have hidden types doing stuff, but I’m bot sure how much it matters.

The wat I see it is that from the reader side, you know you’re using a wrapper and thus need to understand what it does, like any other api. If it uses other tricks internally I’m not sure I would worry about it.

4 Likes

I think SwiftUI is delightful too... but in terms of its implementation, what I'm pointing out is that people are going to get really used to $foo meaning "Binding" because they're using SwiftUI, and then they're going to read $bar and assume that that's a binding too, but it isn't because it's dereferencing a value wrapper from an entirely different property wrapper type.

The fact that a SwiftUI engineer got on stage and told the world that $ means "binding" and not: "$ here is referring to the value wrapper of the property delegate, which in this case is a binding" means that the confusion has already started. I'm not sure if there's any way to fix that... but it's concerning nonetheless.

11 Likes

I'm torn between wrapperValue or not.

I feels like SwiftUI could use BindingConvertible instead of Binding. So it seems wrapperValue is not really necessary there.
Then again, I can see a lot of Property Wrappers not being useful on its own, and probably could leverage wrapperValue.

1 Like

Quoting myself because current discussion derailed from that issue a little.

Cc @Douglas_Gregor do you think we should adopt wrappedValue to reduce possible API conflicts with the generic property name value?

2 Likes

During proposal, I was a big fan of something like #storage(of: propertyName) instead of $propertyName, but SwiftUI demonstrated that #storage(of: state) would not work. So huge thanks for abandoning that idea.

After reading more and more new code however, $state does not click either, sorry. This convention is just different from $0 and $1, which are easily read as “short local references”.

I know this is too late but, if there’s any single chance, may we please consider something else. It can be #wrapper(state), #(state), &(state), @(state), (state), state#wrapper… anything. There must be something better than $state. This is all, thanks in advance!

5 Likes

I agree too that $ being used for "binding" in SwiftUI will introduce the expectation that $ means binding everywhere. Binding isn't even supposed to be the "normal" meaning of $; the normal meaning being referencing the wrapper itself. I think that's a problem.

For a binding, wouldn't the ideal syntax be &foo? A binding isn't that different semantically from passing something inout, except the binding can be passed around more freely. What if the property delegate could enable this with a binding property?

@propertyWrapper struct State<Value> {
    var binding: Binding<Value> { ... }
}
// ... elsewhere ...
@State var allowOverrides: Bool
// ...
Toggle(isOn: &allowOverrides) {
    Text("Allow Overrides")
}

If you pass &foo as an argument and the function parameter isn't inout, the compiler would try to use $foo.binding instead. That'd feel much more natural I think.

(Edit: changed the name of the property to binding. Was referenceValue in the original post.)

3 Likes

That is incorrect. The wrapped value must be a String in that example. You may be thinking of wrapperValue, which doesn't need to exist at all for a property wrapper type.

You mean wrapperValue here.

Corrected. I'm using a toolchain from master, and the names in use in that build don't line up with the current pitch.

1 Like

Is it possible to have a property wrapper return an Optional?

After seeing the user defaults example, I've started toying around with wrapping a third-party document object to provide strongly typed access to properties and relationships through its dictionary-like interface. However, if the document doesn't have a corresponding value, I'd like to return nil, as opposed to some default value, to signify it was unset. I've tried specifying an optional type, but I'm unable to return nil in the value block.

@propertyWrapper
struct DocumentProperty<T> {
    
    let document: MutableDocument
    let key: String
    let defaultValue: T

    init(_ document: MutableDocument, key: String) { //, defaultValue: T) {
        self.document = document
        self.key = key
        // self.defaultValue = defaultValue
    }

    var value: T {
        get {
            if T.self == Date.self, let result = document.date(forKey: key) {
                return result as! T
            }

            if let result = document.value(forKey: key) as? T {
                return result
            }
            return nil // <-- Cannot return nil 
        }
        set {
            document.setValue(newValue, forKey: key)
        }
    }
}

// subclass 

class TaskModel : PersistantModel {
    
    enum DocumentKeys : String{
        case friendlyID
        case completedAt
    }
    
    @DocumentProperty var friendlyId: String?
    @DocumentProperty var completedAt: Date?
    
    override init(document: MutableDocument, database: Database) {
       
        $friendlyId = DocumentProperty<String?>(document, key: DocumentKeys.friendlyID.rawValue) //, defaultValue: "")
        $completedAt = DocumentProperty<Date?>(document, key: DocumentKeys.completedAt.rawValue) //, defaultValue: Date.init())
        
        super.init(document: document, database: database)
    }
}

The ability to access the subclass on which the property is defined would be much more convenient than passing in the document for each property.

Edit: I had modified the PW and model to compile, pasted it into the post and edited to match the non-compiling state - failing to set the properties on the model back to optionals.

Generic type parameters are assumed by default to not be optional. If you want value to be able to return nil You need to declare it as

var value: T? {
    ...
}

Thanks, Nobody. When I tried that, the complier returns the following error without a particular line.

<unknown>:0: error: value of optional type 'String??' must be unwrapped to a value of type 'String?'

I've been thinking long and hard about Property wrapper's wrapperValue. I came to believe that it's necessary, almost natural, to have, and I do have some suggestion:

  • Make $foo usable only when wrapperValue is defined
  • Added init(initialWrapperValue: WrapperType) to allow initialization from $foo, ie. $foo = WrapperType()

tl;dr: this would make a mental model of foo always accessing Wrapper<...>.value/wrappedValue, and $foo always accessing Wrapper<...>.wrapperValue, which would be an easier mental-model.

Longer version:

I was trying to find the relation between Property wrapper, wrapperValue and wrappedValue. And I came up with a conclusion that Property Wrapper is the common storage for the tightly-coupled wrapperValue and wrappedValue. So the base storage is inaccessible on its own, but vent out 2 accessible variables, wrappedValue and wrapperValue.

The distinction between base storage and $foo is rather important since there are cases where base storage is not accessible at all (due to wrapperValue existence).

    ┌───────┐      @Wrapper var foo: Foo
    |Storage|      
    └───┬───┘      // Synthesized
        |          var _base: Wrapper<Foo> // Inaccessible
wrapped | wrapper  var foo: Foo {
   ┌────┴────┐       get { _base.wrappedValue }
   |         |       set { _base.wrappedValue = $0 }
┌──┴──┐   ┌──┴──┐  }
│ foo │   │$foo │  var $foo: ... {
└─────┘   └─────┘    get { _base.wrapperValue }
  ↑ ↓       ↑ ↓      set { _base.wrapperValue = $0 }
access    access   }

foo could be regarded as front-facing interface that is used for most access behavior (get/set), while $foo is back-facing property that is used for the added extra behavior (Reset variable; add Binding, Link, Delegate, Observer, etc.).

We may not always want to have direct access to PropertyWrapper itself if the only useful thing you can do is $foo.alternativeRepresentation, esp. since many PropertyWrappers will be in a form of one-off struct/class that's not very useful on its own, and those that are (useful on its own) don't even need direct access. But of course if direct access is wanted one can do

@propertyWrapper struct Wrapper {
  var wrapper: Self {
    get { return self }
    set { self = $0 }
  }
}

Then, all PropertyWrapper that doesn't need extra functionality can omit wrapperValue, which will in turn disable $foo access.

Furthermore foo and $foo syntactically should be proper variable in its own right. So we should allow for initializer to be done via $foo, to which I suggest init(initialWrapperValue:).

init(initialWrapperValue:) is needed because $foo may not refer to its wrapper type (even in current pitch).

As per composition, foo should work much the same way as in the current pitch, but $foo has extra restriction, that only upto one (1) PropertyWrapper can have wrapperValue. $foo will be ill-formed otherwise.

So, here I conclude my thought process, that PropertyWrapper are one that wrapper around any normal type, and then provide extra functionality via $foo. I tried to decouple PropertyWrapper and whatever $foo is (getting B object from A variable, <SharedStorage?>), but in the end it seems like PropertyWrapper is almost a light extension of <SharedStorage?>. So (as implied in any internet comment), any thought?

6 Likes

Oh yes please. I was one of the people really confused by $ being passed in as binding. I was thinking to myself, is this property taking an inout? I would even accept $& to mean binding but please get rid of the extra magical wrapperValue. Or make it explicit somehow?

I think this is a real problem. We might need something like autoclousure but for bindings.

This solution bring an other layer of complexity, but does not solve the main problem in anyway. This still look magic at call site and you can't know what's going on without having to read the whole type declaration.

The only change it that instead of having to look if wrapperValue is declared, you now have to look if wrapperValue returns self or something else.

I think any meaningful use of $foo requires the users to read the wrapper declaration anyway. Then it wouldn't be too far to look into wrapper's wrapperValue, though I can see the Law of Demeter at play here, so it doesn't solve that magical feeling problem in particular (and I won't call my suggestion ideal).

What I'm trying to do is to make it more consistent that wrapperValue will always be used. It'd feel more magical (and harmful) if $foo sometimes mean the wrapper but the author doesn't intend to have it be accessible, even as private.

I suppose the design can get rid of wrapperValue altogether, and always go with base storage, though I start to wonder if it's a really useful default setting.

We can of course still use $variable.binding to refer to the binding, but the situation will be less than ideal in many wrappers, especially when the design goal is to reduce boilerplate, and I think it's going to be too common to be ignored.

Anyhow, it's gonna be a challenge to come up with a clear declaration when there're 3 types involved (Wrapper itself, value, wrapperValue), but only 2 is required for a full type information (Wrapper and value).

You're right.

My felling is more about not being able to know the type of $foo even at the declaration site. Declaring a variable using a type, and having it magically cast into an other type when trying to access it, is not very intuitive, and is not something we find elsewhere in the language.

I feel like an anti-pattern in a language that forces explicit cast even to convert a small integer into a larger one.

2 Likes