Property wrapper requirements in protocols

Is this guaranteed by the type system though? It‘s the contract of the API to me, please feel free to correct me if I‘m wrong. There are no type system constraints like a where clause for example.

The type system does not guarantee that head is returned for the first element. That's API.

3 Likes

Sure! Of course you must also implement the requirements correctly. I don't think we disagree on that.

But you could use an alternative approach and implement NonEmptyCollection without using a non-optional head + tail, and instead use a single backing array, for all elements, and then have business logic to ensure the invariants.

However, that approach would not use the type system to ensure non-emptyness.

1 Like

Yes, conformances which do not obey the documented semantics of the conformed-to protocol are buggy. I don't think that's a very controversial point.

I'm not arguing that it should be supported in the general case (indeed, that would be impossible). However, I don't see why particular forms of semantic enforcement should just be dismissed out-of-hand. It seems reasonable to me for type authors to think of certain property wrappers as API-level guarantees about how a property is set and returned, rather that just an implementation detail of the backing storage. The existence of projected values, IMO, supports this view—usage of certain property wrappers necessarily exposes a particular API that is as visible as the wrapped property itself.

Yeah, I'm a bit confused by the insistence that the type system is not enforcing non-emptiness here. An identical API in Objective-C would not provide the same guarantee, since head could be nil. As written, NonEmptyCollection is guaranteed by the type system to always contain at least one value. Attempting to implement head in such a way that it does not return a value will be rejected by the compiler (which is why Collection's first requirement is typed as Element?, not Element).

Maybe I'm just understanding terms differently here regarding what is considered to be "enforced by the type system." I certainly don't see how var head: Element is "just API" yet @Wrapper var value: Int is "lifted into the type system."

3 Likes

I will call this a wrapper approach,

protocol Foo {
  @Wrapper var bar: Value { get set }
}

and call this a projection approach,

protocol Foo {
  var $bar: ProjectedValue { get set }
  var bar: Value { get set }
}

We probably talk about them enough to warrant names at this point.


I'm leaning toward the projection approach.

Currently, all protocols only list the functions that needs to be implemented, e.g., functions, accessors, subscripts, etc. At most it would also require associatedtypes.

The projection approach sticks to the current structure. You need to provided $bar accessors, likely via wrapping, but the the choice of actual implementation is still up to the conformer.

The wrapper approach introduces a new type of requirement that bar, not only have accessors, but is also wrapped by Wrapper. There's a lot more information than a list of functions. Also, Wrapper does not appear anywhere at the point of usage. You still use $bar and bar.

Further, if we see see how work arounds are handled, wrapper approach is quite problematic.

  • Using projection approach, if the author wants to require that bar is wrapped by a particular wrapper, they can document it.
  • Using wrapper approach, there is no work around, if the author wants to allow any wrapper to conform to it providing they have the correct projected value. the author can use new variable (like barz in this case), and document that it should be the product of a wrapper wrapping bar. See the exchange down below.

I'm quite ambivalent about the argument that Foo can only work if bar is specifically wrapped by Wrapper. In so far, most wrappers that provides projected value are distinct, @State and @Binding are very far apart in terms of projected and wrapped value. Theoretically, it is possible to have two wrappers that are close enough to equally conform to the protocol, but are implemented differently enough not to have the same type. We don't see such wrappers today. It may be that

  1. They are generally bad ideas, which supports wrapper approach, or
  2. Property wrapper is still not widely used enough for such cases to appear, which supports the projection approach.

I personally feels like one wrong move and we could end up fileprivate-ing this.


Another interesting idea is to allow external classes to wrap the values

protocol Foo {
  var bar: Value { get set }
}

internal extension Foo {
  // We wrap `bar` ourself. The conformer can have their own wrapper too, but that's irrelevant when it's not ambiguous.
  @Wrapper var bar
}

Which would be trickier to pull-off and need a few more things implemented, like wrapping compute properties.

7 Likes

This response to the desired wrapper approach applies just as well to the projection approach. After all, it is possible today to write:

protocol P {
    var _projectedBar: ProjectedValue { get set }
    var bar: Value { get set }
}

If the author wants to specify that _projectedBar is a projection from a property wrapper, they can just document it. In fact, this approach adds even more flexibility, since the "projection approach" requires that the ProjectedValue instance be generated by some property wrapper, since $ properties can't be declared manually. Using this approach, conformers are free to generate the ProjectedValue in whatever way makes sense for their type!

Is it particularly useful to specify that a property is projected from any wrapper that projects a certain type? Consider:

@propertyWrapper 
struct ReciprocalProvider {
    var wrappedValue: Double
    var projectedValue: Double { 1.0 / wrappedValue }
}

protocol P1 {
    var value: Double { get set }
    var $value: Double { get set } // What is this?
}

protocol P2 {
    @ReciprocalProvider var value: Double
}

The P2 protocol, to me, seems far more reasonable—the first is general to the point of ambiguity.

I don't see these two approaches as mutually exclusive. In many cases you may not care what property wrapper wraps the specified type, and I think that wrappers which project Binding<T>s are a good example of this. If the wrapper approach is supported, then it seems obvious to me that the projection approach should be supported as well.

To me, this pitch boils down to one main question:

Is the type of property wrapper considered part of the API of a property (and if not, should it be), or is it purely an implementation detail?

This is a technical question in addition to a language-philosophical one. E.g., it is not possible today to declare an internal (or public) wrapped property that is wrapped with a private wrapper type. The following is an error:

struct S {
    @propertyWrapper
    private struct W {
        var wrappedValue: Int
    }

    @W var x: Int // Error: Property must be declared private
}

So it seems that @W is not just an implementation detail as it exists today. And yet, declaring W internal and x public is perfectly acceptable, so it's certainly not part of the public API for S.x. Edit: This applies if x is public and W is internal, also.

2 Likes

Hmm, I didn't think about that. It's still somewhat cumbersome compared to the other side, but at least not impossible. Anyhow, I'll edit my comment to include that.

It sure does. It also means that this thread is more of a convenient tool than strictly adding a new capability, which is all well and good, but I'll need to think about the ramification of that some more.

That's a good question. I don't know. As said, not seeing this pattern in the wild can also be an indication that it's not a good approach. I think property wrapper is very diverse, and is too new to settle down with some best-practice yet.

It's an interesting question. Since the property wrapper has been treated as a sugar for declaring bar, _bar, and the impossible $bar, I'd be inclined to think no.

(it's been happening to me lately, when someone raise an interesting case, but it doesn't work on my machine)

This does not compile on my Xcode 11.3.1 (11C504) with this error:

Property cannot be declared public because its property wrapper type uses an internal type.

This one is on myself, not you! My playground must not have reloaded properly—when I went back and tried the same code, I got the same error. I'll update my post!

In any case, that's even stronger support for wrappers-as-API, though there's a further question: even if the wrapper type is visible, can the type information on the wrapped property be recovered in order to allow for a conformance? E.g.

// Module A

public struct MyView: View {
    @State public var text: String
}

// Module B

protocol TextWithState {
    @State var text: String
}

extension MyView: TextWithState {} // Is this possible?

If this information isn't present in the module binary, is it possible to add it in an ABI compatible way, if not, we would need some sort of syntax like:

    public @State public var text: String

to signify that a module actually wants to export the type info of the wrapper. Perhaps @Douglas_Gregor would be able to comment a bit more on the ABI implications here...

I'm pretty sure currently there is no information of wrappers on ABI level. Things like this

@State public var text: String

compiles to one storage _test and two (four?) accessors, test, and $test. I don't' see much room to insert the wrapper information.

One way I do see is to allow custom access level to _test which is one of Future Plans. It's not a true wrapper in a sense that conformer can still do arbitrary things to _test and test, but that'd be very much getting out of the way to achieve.

2 Likes

Sure, and I would expect that to continue to be the case even if this capability were added. However, that doesn't mean that there isn't/couldn't be some ABI-level annotation that says "text was wrapped by SwiftUI.State" that is simply ignored by platforms that don't know recognize it. I'm out of my depth in discussing ABI tricks, but that's definitely needed context that informs this feature.

1 Like

I found this proposal, because I have a real-world issue similar to this code:

In my case, this View is a protocol for many different possible List cells. I want to extend this protocol to provide common code -- here is a fake example to illustrate the problem:

extension ViewWithState {
   func common() {
       self.value = 5
   }
}

This cannot compile in real code, because the compiler thinks that this line mutates self. If I could express that value is @State, then this would know that implementations of the protocol do not mutate self.

Because SwiftUI Views heavily use property wrappers, it is extremely hard to use protocol/extensions to provide common code.

@loufranco Make it var value: Int { get nonmutating set } in the protocol requirement.

3 Likes

This worked perfectly -- thanks. It also points to a general idea that you might want to describe the requirements of the property in a protocol, not a specific property wrapper that it uses.

1 Like

Jumping on this discussion, as I feel there is something important missing regarding the property wrapper, especially regarding the projectedValue that has been already discussed during the original property wrapper proposal and here.
By sharing a very relatively classic use case of app development that I encounter, I would like to show how convenient it will be to be able to declare a projectedValue requirement on a protocol variable at least - if it is not possible to specify this requirement on the whole property wrapper to use on the protocol.
Today, I face a blocker regarding the adoption of property wrappers:
Good practice in a relatively big project is to have protocols for injecting and connecting the different app layers such as services with views or view models. In particular, they allow mocking these types for unit tests or for SwiftUI previews. This is a must-have on a large application. Combine introduced @Publlished, a very convenient property wrapper that exposes a publisher by the projected value as well as exposing the value itself in R/W. This publisher makes our API very clean and straightforward (only one variable). But the moment we introduce protocol, we no longer can benefit from the projected value syntax ($).
So we need ether

  • to declare two variables for the same data (the r/w value and the publisher),
  • to do a hack that consists of creating a class that contains the @Publlished variables, and make the protocol inherit from this class. Allowing the share @Published vars with all the implementations of the protocol by inheritance.
    I think it will be nice to have a sugar syntax that defines directly on the variable the projection requirement. Even if allowing to use $ in a second variable declaration will solve the use case.

To conclude, If you have any alternative to share for my use case, I would love to read about them too :slightly_smiling_face:

+1 for this.
Just two questions:

  1. Will it be possible to stay with current behavior, if I want to hide the details from protocol, that property is wrapped?
  2. Should we have a possibility to hide projected value?

+1 on this proposal. We'd like to be able to use @Published in a protocol.

2 Likes

The projection approach seems to be a no-brainer.

The only thing (seemingly) in the way of doing this is the "Cannot declare entity named '$x'; the '$' prefix is reserved for implicitly-synthesized declarations"

While this error makes sense from a language perspective when you are declaring a concrete type, I see no real reason that you can't have a protocol require a projected property in this way.

Should this be put forward as proposal itself? To me it makes sense whether or not the wrapper approach was ever implemented.

5 Likes

Hi, it’s been a while, but any news on this subject? I, too, miss the possibility to add property wrappers in protocols…

4 Likes

To give this a bit of a push, I've implemented the "protocols with projected values" approach here:

8 Likes

Spectacular! :heart: I'll have a considerably improved and fleshed-out revision of the proposal up very soon, to go with this.

5 Likes