SE-0258: Property Wrappers (third review)

+1!

I was onboard with the general idea of the proposal from the first revision, but off-put by how cryptic both $value and $$value existing at the same time would be during the second revision, but the changes to underscore the backing property and to project onto $value are very nice and easy to explain to someone unfamiliar to the feature (i.e. "value is the original, wrapped var, $value is Foo<Baz>.projectedValue if present, and _value is the actual Foo<Baz> storage generated by the compiler and always private"). It is nice because _value has a private-ish connotation and is distinct enough from $value to make it easy to distinguish.

This is overall a very nice addition to the language, has matured well since the initial pitch, and is ready to land now, in my opinion.

3 Likes

It seems I won't have the time to cobble together a complete review. But I'd like to address two issues briefly:


The property wrapper type must have a property named wrappedValue , whose access level is the same as that of the type itself.

That initializer must have a single parameter of the same type as the wrappedValue property (or be an @autoclosure thereof) and have the same access level as the property wrapper type itself.

As with the wrappedValue property and init(initialValue:) , the projectedValue property must have the same access level as its property wrapper type. For example:

Nit here: it should have the same effective visibility as the type itself. Not that it will be a common occurrence, but a private type would need to declare its members fileprivate or more to be usable. The rules here should follow that of access levels elsewhere in Swift.


I agree with previous commentators that this leads to a usability issue where $foo and _foo are both visible and interchangeable. Moreover, there is the issue that _foo may clash with an existing member, and the issue that it is not visibly a synthesized member. All three issues would be addressed if instead the names were $foo and $_foo, since the latter would be clearly 'demoted' in importance.

2 Likes

That takes away the connotation of "projection" for $, which would be a shame.

Since foo is already a "derived" property with a backing storage, it would be very unwise to have an unrelated property named _foo already, so I think this is a very minor limitation. Also, _foo is already a well established name for the backing of foo.

I'm +1 on the proposal as it now stands. I also agree any further work could be left in "future directions"

1 Like

+1

The new naming is much less confusing!

1 Like

I don't see the point exposing the property wrappers to the clients, unless there is a public projection.
A property wrapper without (public) projection is and must stay an implementation detail IHMO.

Just like Swift by nature don't expose if a property is a competed or a stored property.

1 Like

And thinking about it, I wonder if even when there is a projection, the wrapper should be exposed or not.
Maybe the generated interface should expose the projection directly. The type of wrapper used to generate the projection is an implementation detail too.

class Foo {
   @Wrap public var a: Int = 0
}

would give

class Foo {
  public var a: Int { get set }
  public var $a: Projection { get }
}
1 Like

I see an issue here in that different wrappers may vend a different projection, even if the type of that projection is the same. Consider:

struct Printer<T> {
    var preamble: String
    var value: T

    func print() { print("\(preamble): \(value)") }
}

@propertyWrapper struct Wrapper1<Value> {
    // ...
    var projectedValue: Printer<Value> { Printer(preamble: "Wrapper 1:", value: wrappedValue) }
}

@propertyWrapper struct Wrapper2<Value> {
    // ...
    var projectedValue: Printer<Value> { Printer(preamble: "Wrapper 2:", value: wrappedValue) }
}

struct MyStruct {
    @Wrapper1 var a: Int = 1
    @Wrapper2 var b: Int = 2
}

The compiler would synthesize an interface of:

struct MyStruct {
    var a: Int { get set }
    var $a: Printer<Int> { get }
    var b: Int { get set }
    var $b: Printer<Int> { get }
}

Sometimes, the actual construction of the projection might be an implementation detail, but in cases where it's significant, hiding the type of the wrapper itself would force all clients of Wrapper1 or Wrapper2 to copy any documentation describing its projection into the class themselves, or force them to document the property as "wrapped by a Wrapper1/Wrapper2".

IMO, this is a flaw of the whole projection system in general, and the more you hide the wrapper type the worse it gets. I would love if we could do away with it all, have the storage be accessed as $foo, and let SwiftUI call sites that take a Binding<T> provide an overload which takes a State<T> and call .binding internally so that WWDC sessions don't break. I don't know if this would be possible with the current implementation of State, though.

On the whole, though I am +1 on this feature as it stands. Having a consistent name for the storage is a great improvement from the last iteration, and this finally feels like a polished feature for me even if there are some design choices I disagree with. Thanks to Doug and John (and the rest of the Core Team) for all their work getting this feature through the review process!

If we're going that route then access_level(projection) should do the trick later. Now every property wrapper type that would be put into OS frameworks should be designed so that it wants to expose projections, we won't be able to hide them in the future as it would be a breaking change.

@Douglas_Gregor I guess you can ignore my post from above.

I actually see this as an advantage; it means there's a natural path for synthesizing against an existing property if we choose to allow that. (Obviously the type would have to match.)

6 Likes

Do you mean like CodingKeys in Codable? The compiler uses it if it's there, and synthesize it if it's not. I do like that, it does provide a good amount of customization.

It's been a requested future direction that programmers be able to supply the storage themselves, yes. (Presumably it could be computed — or an alias to other storage, if we add such a feature.)

5 Likes

The problem with this is that SwiftUI encourages the use of default initializers on structs. Making users write explicit generic initializers for all their bindable views would make it a lot less visually lightweight.

(In other words, projections are being used as a hack around the absence of extensible implicit conversions in Swift. This could easily have been achieved with a prefix operator, but here we are.)

2 Likes

I have a question about property wrappers and protocols, also related to the hiding of the wrapper as an implementation detail. In beta 2, it looks like the following is disallowed:

protocol Foo {
    @Published var bar: Int { get } // error: Property 'bar' declared inside a protocol cannot have a delegate
}

If we are always considering the wrapper type to be an implementation detail, it makes sense that this would be disallowed. But this is also disallowed:

protocol Foo {
    var bar: Int { get }
    var $bar: Published<Int> { get } // error: Cannot declare entity '$bar' with a '$' prefix
}

This would have the nice advantage that any wrapper type which supplies the proper projection could be substituted in for Published, even though it makes the protocol definition a bit more verbose.

Is there any plan to support this? If projections are "equal in importance to the wrapped value," it seems like there should be some way to specify in a protocol that there must be a certain projection available. The workaround of adding a var publishedBar: Published<Bar> requirement dilutes the intent of the protocol and makes conforming to it cumbersome. Is there a different way to achieve this that I'm not seeing?

3 Likes

The provided initializers which take a State<_> would be there only in SwiftUI-owned types for compatibility with existing WWDC sample code and sessions. Users of SwiftUI would be free to copy this paradigm if they want, but I was imagining that the canonical way would just be to force users to write $foo.binding if they wish to create a binding to their state.

Sure, but we already know that isn’t going to happen.

1 Like

Suggestions:

  1. Could the same name be used for wrapping and unwrapping?
    i.e. init(wrappedValue: T) and var wrappedValue: T.

  2. Or could there be an unlabelled initializer to wrap, and a no-parameter subscript to unwrap?
    i.e. init(_: T) and subscript() -> T.

Overall +1

I am concerned about the private backing storage taking on a valid identifier that can collide with other identifiers that start with _

Would you consider _$myprop ?

Can you or other that share the same concern elaborate on why this is an issue? Can you provide a concrete example why or where such collision would ever occur? Why would you have a wrapped property foo but also a totally different _foo property which is unrelated?

4 Likes

I've been following the development of this feature from the start and would like to thank the core team and proposal authors for being receptive to the feedback.

After reading 3rd proposal and looking over the implementation, I am a +1. Specifically, very happy to see the following feedback taken into consideration:

While there are multiple opinions from community members on things quoted above, proposed changes resonate with me personally.

I also agree with this rationale.

Cheers!