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

I love this idea! The way to stop people from thinking that the default thing happens ($foo means the wrapper of foo) is to not have the default. $foo isn't the wrapper anymore, so we shouldn't pretend that it is. We may also change the name from wrapperValue to something like wrapperRepresentation to further distance $foo from the wrapper itself.

I personally have no problem with $foo having an arbitrary type.

The $ sign will make some wrappers look magical to some users. As with all magical wands, those users will learn spells and tricks. The fact that they do not quite understand what's happening is OK, as long as they have read a little documentation, or taken inspiration from code written by a more experienced user. Just as in a regular wizard school.

SwiftUI wrappers clearly belong to this category.

Fortunately, wrappers are not only magic. Some of us do not like magic at all. You have surely already read some blog posts that try to decipher SwiftUI @State, @Binding etc. Those are fascinating, because they shed some light on the amount of tech that is hidden behind those simple names.

And here is the real value of $ having an arbitrary type: opaqueness. The reasons why the wrapper designer has chosen the type of $foo is none of your business. He knows better. You may never know why. But you may be delighted by the API, even after all magic has vanished.

10 Likes

I absolutey love this proposal (been following since day 1) and I am really impressed by its usage in SwiftUI.

However, I am really sad to see the following change:

This breaks my desired use case and I'm hoping / wishing there is some way we can fix it.


Here's a simplified preview of what I'd like to use property wrappers for in the next version of vapor/fluent, and ORM for server-side Swift:

Assume the following types, where Field is a property wrapper representing a field on an ORM model. The Field backing takes care of pulling the associated value from database output as well as storing new input. The details of how this happens aren't important, though. The key here is that the Field property wrapper stores the String name of the field. In other words, the column name in the database.

@propertyWrapper
struct Field<Value> where Value: Codable {
    var name: String

    init(_ name: String) {
        self.name = name
    }

    var value: Value {
        get { /* load from database output */ }
        set { /* set to database input */ }
    }
}

protocol Model {
    init()
}

final class QueryBuilder<ModelType> where ModelType: Model {
    var filters: [String: Any]

    init(_ type: ModelType.Type) {
        self.filters = [:]
    }

    func filter<Value>(_ keyPath: KeyPath<ModelType, Field<Value>>, _ value: Value) -> Self {
        let name = ModelType.init()[keyPath: keyPath].name
        self.filters[name] = value
        return self
    }

    func run() {
        print(self.filters)
    }
}

Previously, with property wrappers not being private, I could use a key path to the backing store to fetch the name of the field during query building.

struct Planet: Model {
    @Field("_id") var id: Int?
    @Field("planet_name") var name: String
}

QueryBuilder(Planet.self).filter(\.$name, "earth").run()

This test prints the following value for QueryBuilder.filters:

["planet_name": "earth"]

This works great in Xcode 11 beta 1, but now breaks on master where the property wrapper backing is private.


I'd really love to be able to use property wrappers for this feature in the upcoming version of Fluent for Swift 5.1. But, with the latest restriction to make the backing store private, my use case is broken.

I know this feature was put into "future direction", but is there any way we could reconsider? I think having the backing store get the same access-level as the base property made a lot of sense. It allows for much more flexibility when using property wrappers until we get to a point where we can design a more complex access-level system for them.

10 Likes

I am 100% in favor of putting this back to "use the access level of the base property"; I understand the case for a private backing store but I agree with Tanner that it's too early in the feature's lifetime to restrict its usability this way.

8 Likes

The current draft supports internal(wrapper) if you don't mind writing out the access level of the wrapper manually.

1 Like

I agree with @graskind about it being too early to restrict the feature this much. Totally understand the initial reasoning but would really love to see it reconsidered.

3 Likes

:heart:


That could work, but makes the point of use a lot uglier:

struct Planet: Model {
    @Field("_id") internal(wrapper) var id: Int?
    @Field("planet_name") internal(wrapper) var name: String
}

IMHO, it makes more sense for the backing-store to have the same access level as its value by default. If I write @Foo public var, it seems logical that the "@Foo" part of this is also public--it says public right next to it. Especially when considering things like @Atomic, @UserDefault, and @Lazy where there are useful properties and methods on the backing store. It's a shame to not be able to access any of that.

Furthermore, there's already a clear convention in Swift for keeping implementation details private:

@Atomic private var _foo: Int
public var foo: Int {
    get { return _foo }
    set { _foo = newValue }
}

IMO, this pattern would be a better solution to wrapper access-level anyway: There would be no new access-level keywords to learn or use and someone reading the source would know exactly what is going on.

This pattern also requires less mental load to understand a Swift API: if something is marked @Foo var: x Int, then it is a "Foo backed Int". This is fundamentally different than a plain Int, var x: Int. With this distinction, there is no need to understand an additional access-level and all the various permutations that come along with it.

I think relying on this pattern for keeping the backing-store private would be a better first step than starting out with the backing store and base value having different access-levels. If the goal is to be conservative, as the proposal states, then the more conservative option would not force us to go down a road where we must end up supporting distinct access levels.

9 Likes

i'm okay with $ having implementation-defined type too, but if that's so, then $foo should never be synthesized automatically. Right now, $ basically means "the value wrapper, except when it's not" which is really the worst of both worlds. Either give $ a standard meaning, or make it completely implementation-defined.

9 Likes

Because the problem is solved is everywhere in Swift. Looking at people's code online, I see tons of boiler plate revolving around:

  • getters/setters that interact with UserDefaults
  • dealing with deferred initialization
  • interacting with observer/publisher patterns, such as delegates, Rx observables, etc.

If all those copy/pasted patterns could be codified into types, this is what it would look like, and it's wonderful.

4 Likes

I agree with Tanner and Gwynne! This would be pretty nice.

Playing with the Xcode beta, it seems that it still works this way; access to the wrapper follows access to the property.

That wouldn't be any better as the storage will be exposed to public if you forget to change it, that is definitely not what we want. If anything else than 'private by default' it should use the existing access convention and be 'internal by default'.

4 Likes

+1

+1 on internal by default, it feels like foo and $foo are separate variable, and so should be treated as such. So $foo without scope should be internal.

5 Likes

Another set of useful additions would be to add additional metadata about the property wrapper that is available internal to the wrapper itself. For example, the ability to access the name of the property as a string or key path ($keyPath ?), in the case where the property matches some internal aspect of the database. That way, passing the key could be optional. Also, the addition of a variable ($propertyHost ?) that refers to the struct or class the property is defined on. This way, we could back the property using storage in the class or struct, without needing to pass it into the attribute or initialize it later. (See my above comment)

Yeah, I agree 100%. I'm able to work around those limitations for now at least using Mirror and reference type wrappers, but I'd love to see them added in the future. And indeed, those are listed in future direction, so I'm fine punting those.

The major issue for me at the moment is the private-by-default decision which is not possible to work around and makes the feature unusable for my case.

What if a wrapper could specify its own default accessibility level? Some, like bindings, will want to be more accessible, while others, like "atomic" maybe, are more of an implementation detail and should be private.

3 Likes

Maybe I miss something, but I didn't find any way to have different access levels between the property and the synthesised $ property

struct Foo {
    @State public var bar: Int
}

that would be the equivalent of

struct Foo {
    public var $bar: State<Int>
    public var bar: Int {
        get { $bar.value }
        set { $bar.value = newValue }
    }
}

How to do

struct Foo {
    private var $bar: State<Int>
    public var bar: Int {
        get { $bar.value }
        set { $bar.value = newValue }
    }
}

Is there already a way in the current implementation? Is the need shared? I found an use case (currently using RxSwift where I want to expose the int but not the BehaviorRelay to the rest of the world)

In Xcode 11 Beta 1, the backing property is the same access level as the base property. On master, the backing property is private.

1 Like

And in Modern Swift API Design EDIT: around 28:50 (which, as far as I can tell is where the $foo is first explained), it is said that "the [backing storage] is an instance of the property wrapper type," without any mention of the fact that this is not a general fact, but a product of the design of LateInitialized specifically.