SE-0258: Property Wrappers (second review)

The review of SE-0258: Property Wrappers (previously called "Property Delegates") begins now and runs through June 24th, 2019. This is the second review of SE-0258, which was returned for revision subsequent to its first review.

Doug Gregor has provided the following list of differences from the previous revision:

  • The name of the feature has been changed from "property delegates" to "property wrappers" to better communicate how they work and avoid the existing uses of the term "delegate" in the Apple developer community
  • When a property wrapper type has a no-parameter init(), properties that use that wrapper type will be implicitly initialized via init().
  • Support for property wrapper composition has been added, using a "nesting" model.
  • A property with a wrapper can no longer have an explicit get or set declared, to match with the behavior of existing, similar features (lazy, @NSCopying).
  • A property with a wrapper does not need to be final.
  • Reduced the visibility of the synthesized storage property to private.
  • When a wrapper type provides wrapperValue, the (computed) $ variable is internal (at most) and the backing storage variable gets the prefix $$ (and remains private).
  • Removed the restriction banning property wrappers from having names that match the regular expression _*[a-z].*.
  • Codable, Hashable, and Equatable synthesis are now based on the backing storage properties, which is a simpler model that gives more control to the authors of property wrapper types.
  • Improved type inference for property wrapper types and clarified that the type of the wrappedValue property is used as part of this inference. See the "Type inference" section.
  • Renamed the value property to wrappedValue to avoid conflicts.
  • Initial values and explicitly-specified initializer arguments can both be used together; see the @Clamping example.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via email or direct message on the forums. If you send me email, please put "SE-0258" somewhere in the subject line.

Notice as of June 19th: further revisions to this proposal are expected; the review period will be extended when the revised proposal is ready

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thank you for contributing to Swift.

John McCall
Review Manager

22 Likes
  • What is your evaluation of the proposal?

+1 with reservation about the complexity. I've been following the discussions and read some of the code in the tech demo for SwiftUI.

I am worried about the complexity of the backing storage specially when combined with other swift features.
There is now:
wrappedValue which is exposed private as $$someVariable
wrapperValue which is exposed internal as $someVariable

I think this will lead to the current Optional infinite nesting?????

 When a wrapper type provides `wrapperValue`, the (computed) `$` variable is `internal` (at most) and the backing storage variable gets the prefix `$$` (and remains private).
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • 1
  • Does this proposal fit well with the feel and direction of Swift?

+1

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

no

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More than I should

I'm all for this feature, as I've state in previous threads on the topic.

One question I have that wasn't addressed in the other threads is: are global/local "properties" (variables?) going to support in this version, or are those going to be pushed to future directions? The proposal mentions they will be supported, but the most recent master snapshot says they are not yet supported.

1 Like

We'll get them implemented, although probably not immediately. Local variables are different enough in the representation that it's going to take some compiler surgery to get this working. (lazy still doesn't work for local variables, either)

Doug

4 Likes

I think the access to backing storage can be quite confusing.

It is $ when wrapperValue is absence, but become $$ when wrapper is present. I'd prefer to have one always be backing storage, and another to always be wrapperValue (if present).

5 Likes

An impressive amount of work has gone into the design and implementation of this proposal. It has reached a very polished state that feels ready for inclusion in the language.

This version of the proposal provides compelling features that fit well with the rest of the language and will allow rapid exploration of new behaviors through library implementations rather than needing to bake them into the compiler. I'm certain that within a year or two there will be many uses of wrappers throughout the standard library that will be regularly used by all Swift programmers whether or not they are writing their own wrappers. The common Swift philosophy of progressive disclosure works well here, with newcomers to the language able to make use of existing wrappers as easily as they do existing compiler magic (e.g., lazy) without needing to know how they work. The use of a type name in the @Foo attribute syntax (and the "jump to definition" feature in Xcode) allows for easy discovery of the implementation of a wrapper.

I feel that this proposal strikes a good balance between including most of the highest-priority items related to this feature while leaving appropriate options open for future expansion. Many of the items listed as Future Directions seem likely to be brought up again in the near future but some of them will only become clear after we have more experience actually using property wrappers in real code.

Yes, there are many compelling use cases already and it's easy to imagine dozens more.

It feels extremely Swifty to me. Not only does it fit in well with the existing language but it also provides a good model for thinking about future macro-ish features.

n/a

I've read each version of this and followed all the threads. I've played around with the version included in the Xcode beta.

1 Like

That was my initial thought but then when I thought about the experience at point of use I saw the reasoning behind it. The idea is that $foo will generally be what people use in most cases, without the end user needing to know whether or not wrapperValue is defined. In most use cases I can imagine that's what's most convenient.

I could see a case for making the backing storage available as $$foo in all cases (with both $foo and $$foo referring to it if there is no wrapperValue).

Edit: thanks @lantua for the pointing out the dumb typo

2 Likes

Took four read throughs mostly due to being insure about the $ syntax.

Overall I think this fits well. It does feel Swifty, but different. The different always concerns me, but I think the changes are justified with valid use cases.

This proposal feels more solid and holistic.

+1

Not wrappedValue, but the backing storage. :slight_smile:
wrappedValue is accessible without any $.

Of course, you're right.

All the renaming of things between versions is making my head hurt. As is the single character difference between wrapped/wrapper.

Overall I am for this feature being accepted. However, I wish to complain about the color of one of the bikesheds, so please bear with me.

With subtle and fine distinctions, such as between wrapperValue and wrappedValue, careful attention needs to be paid to documentation and language -- how users will come to know and experience this distinction. I'm concerned that people will come to understand this genuinely useful and laregly non-magical feature as its opposite. I dont have any particular solutions, but instead I offer that one of the big contributors is a consequence of the fact that @propertyWrapper is built-in: the interface it asks of implementors is not declared anywhere with in-language constructs.

It's probably only because I was messing with some OCaml and ReasonML exercises recently, but it seems like Swift keeps reaching towards different syntactic forms that could be expressed with modules or module "functors". Not just property wrappers but function builders, variadic generics, parameterized extensions, even higher-kinded types. In this case, having @propertyWrapper become merely a module functor would clarify exactly the different signatures clients could implement.

Thanks for your time & attention.

tl;dr +1 on the feature, -0 on wrappedValue / wrapperValue, pls to haveing modulez

2 Likes

Note that there are toolchains that implement the proposal:

Expect bugs! And when you find them, please report them at bugs.swift.org.

Doug

2 Likes

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

+1

Though I am not at all a fan of how close wrappedValue is to wrapperValue. the distinction is in the middle of the identifier. This will be frustrating to teach and frustrating to remember.

12 Likes

Also +1 on this issue. I'm not seeing a great reason to have property wrappers be limited in this way. If users want to protect the storage then they can specify the access level, that way it reads as an explicit action/choice that differs from the default and that brings clarity.

3 Likes

Very loudly agreed. I'm -1 on this proposal until this issue is addressed.

15 Likes

Can't one effectively raise the visibility level of the synthesized property simply by declaring a (public) wrapperValue getter that returns Self?

No, more complex than that. You can raise it from private to internal, but not past. There’s no way in the current proposal to make the backing store public.

1 Like

I found a few places in the proposal text and examples which look like inconsistencies. If they are, I think it would worth fixing them to help people understand this fairly complex proposal:

Proposed solution

var $foo: Lazy<Int> = Lazy<Int>(initialValue: 1738)
var foo: Int {
  get { return $foo.value }
  set { $foo.value = newValue }
}

Shouldn’t the wrapper property be private?

Composition of property wrappers

var $path: DelayedMutable<Copying<UIBezierPath>> = .init()
var path: UIBezierPath {
  get { return $path.value.value }
  set { $path.value.value = newValue }
}

Same comment as above.

Initialization of synthesized storage properties

This section does not mention the initialization that allows declaring the wrapper with arguments while also receiving the initial value through the property declaration. So there are technically four ways to initialize a property wrapper.

Secondly, this section also mentions:

When there are multiple, composed property wrappers, only the first (outermost) wrapper may have initializer arguments.

This sounds weird because I don’t see why the limitation exists. It is also in contradiction with an example a few sections below where it’s the last inner most wrapper that has initialized argument.

@A @B(name: "Hello") var bar = 42
// type inference as in ...
var $bar = A(initialValue: B(initialValue: 42, name: "Hello"))
// infers the type of '$bar' to be 'A<B<Int>'

Detailed design

I’m if I’m not mistaken, the proposal doesn’t clearly state that the generated property wrappers are private. If that’s the case, could a small section be written about that under Detailed design?

3 Likes

As to the proposal in itself, I am super happy with how much it has improved since it’s earlier iterations. I’m especially delighted about wrappers being able to be declared with arguments as well as being initialized with the initial value.

Nonetheless, I have the feeling that it would be important to address the accessibility level question before we bake in a private default. Like @tanner01, I would really prefer the wrapper (and wrapperValue) to default to the same accessibility level as the property and allow tweaking them with a private(wrapper) modifier.

5 Likes