SE-0258: Property Wrappers (second review)

The backing storage access level rules need more thought before we should consider accepting this proposal.

Proposed Change

I suggest we make the following change to this proposal:

- • Reduced the visibility of the synthesized storage property 
-   to private.
+ • Synthesized storage properties have the same access level
+   as the base property 
- • When a wrapper type provides wrapperValue, the (computed)
-   $ variable is internal (at most) and the backing storage variable 
-   gets the prefix $$ (and remains private).
+ • When a wrapper type provides a wrapperValue, the backing
+   storage gets the prefix $$, and wrapperValue is accessible with
+   $ prefix

Explanation

I've been following this proposal since day one and I'm a huge fan. I'm extremely grateful for the amazing work Doug and others have put into this. Property wrappers will open the door to tons of new functionality in Swift and I can't wait to use them in the OSS packages I maintain.

However, we really need to simplify the access level rules. I understand wanting to be conservative with access level as a first step, but I think simplicity and usability are much more important. Especially considering the future direction of property wrappers, these complex access level rules will be difficult to work around.

For example, here is good use case for property wrappers that is not possible with the current state of access control:

Module A:

public final class User: Model {
    @Field public var id: Int?
    @Field public var name: String
}

Module B:

import A

// error: $name is not accessible
User.query(on: database).filter(\.$name == "Foo").all()
//                                ^

As a developer, this error is confusing to me. I've marked everything as public, why can I not access the properties on my type? Is there any way to make the backing storage accessible? As of this current proposal, the only way to achieve this would be:

public final class User: Model {
    @Field public var id: Int?
    public var idField: Field<Int?> { 
        return self.$id 
    }
    @Field public var name: String
    public var nameField: Field<IString> { 
        return self.$name 
    }
}

This is obviously far from ideal. It would be easier to not use property wrappers in the first place.

Suppose we do add a way to control access level in the future, how would we make the wrapper public? Maybe something like this:

public final class User: Model {
    @Field public(wrapper) public var id: Int?
    @Field public(wrapper) public var name: String
}

This would also be confusing to me. Why do I need to declare public twice on this property? It would make much more sense to opt-in to restricting access:

public final class User: Model {
    @Field private(wrapper) public var id: Int?
    @Field private(wrapper) public var name: String
}

Looking at this API, it's much clearer what is happening and makes more sense: This is a public property, so if I want to make part of it private, I should specify that.


Part of what makes Swift so great is its "simple by default" philosophy. For example, you can declare var x = 5 without needing to specify a type. Swift will do what makes sense by default. However, when you need more complexity in Swift, you can always opt-in: var x: UInt32 = 5. Same goes for all of Swift, but there is even precedent with access level here:

final class Foo {
    public private(set) var x: Int
}

Again, simple by default, with the possibility for more complex rules if you need. Why shouldn't property wrappers behave similarly?


Finally, I want to note that the latest change to have wrapperValue be internal by default is a step in the right direction. This at least unblocks a lot of potential use cases for this feature. But let's go one step further and remove the complexity and restrictions entirely.

There are a ton of awesome ideas popping up for property wrappers: @Lazy, @UserDefault, @Atomic, just to name a few. All of these wrappers have useful properties and methods on the backing store. Please let developers access this functionality!

35 Likes