Pitch: Property Delegates

Do we really need to access the backing storage? I mean the whole idea is to "accommodate several important patterns for properties" and correct me if I'm wrong, the backing storage is going to be private or internal in most cases. There aren't many use-cases that we really need to access the backing storage.
That being said, this feature cannot stop anyone from writing this code:

var fooStorage: Lazy<Int> = .init(initialValue: 42)
var foo: Int {
    get { return fooStorage.value }
    set { fooStorage.value = newValue }
}

The whole idea of having $ as prefix of backing storage is cool, but I think it's just overdoing it.
If the backing storage needs to be exposed, I prefer it being defined explicitly rather than being synthesized implicitly. Either-way delegation would still be possible:

@staticAttribute(usage: [.property])
struct Delegated<Delegate: PropertyDelegate> {

    var delegate: Delegate

    init(to delegate: Delegate) { ... }
}

class Test {

    public var dataStorage = Lazy<Data>()

    @Delegated(to: dataStorage)
    private var data: Data
}

Another approach would be to define the synthesized storage with different access-level. (but my guess is, it will add a lot of complexities to the compiler since using $ as prefix of variable is not allowed):

class Test {
    @Lazy var data1: Data
    
    private var $data2: Lazy<Data>
    @Lazy var data2: Data
}

will result in:

class Test {
    internal var $data1 = Lazy<Data>()
    
    internal var data1: Data {
        mutating get { return $data1.value }
        set { $data1.value = newValue }
    }
    
    private var $data2 = Lazy<Data>()
   
    internal var data2: Data {
        mutating get { return $data2.value }
        set { $data2.value = newValue }
    }
}
4 Likes

Thanks for your feedback! I do agree it's more verbose, but the access level parameter would be defaulted, so in most cases it would be just:

@Delegated(to: Lazy.self) 
public var data: Data

If we choose to call it storage, it becomes even simpler:

@Storage(Lazy.self) 
public var data: Data

As others have noted, access to the storage would rarely be need, so we could actually default the access level parameter to unexposed (hidden). A way to provide access could be to name the underlying storage property:

@Storage(Lazy.self, named: "_data") 
public var data: Data

That would expose it to max(internal, access-of-declared-property) visibility as self._data, but one could also change the access level:

@Storage(Lazy.self, named: "_data", visibility: .public) 
public var data: Data

Storage would also provide an initializer that accepts instances

@Storage(Lazy { ... }) 
public var data: Data

and maybe the one that allows key path references from self

@Storage(\.core.data) 
public var data: Data

I would be happy with built in @propertyDelegate of course, but I'm just trying to make sure we are not over-engineering this when a simple static attribute interpreted by the compiler provides more flexibility and solves most of the cases discussed so far at the cost of some verbosity.

I do agree it might be a too high price to pay, but it's worth considering.

1 Like

With access modifiers and attributes like @available, Swift is already there. Moving this one feature later (like the by syntax was doing) won't really solve the problem you're describing.

This makes the delegate type central, which is not what we want: we want the property type to be central, and the delegate type to be used for the backing property.

Doug

5 Likes

I don’t like this @ syntax because it doesn’t explicitly name the fact the storage of the property is delegated to or the storage of the property is provided by or from elsewhere. It’s harder to understand and read because the @ is confused with other compiler concepts and it’s harder to explain (express into a sentence of English, say) and teach.

With respect to exposing the storage using the $ syntax, could this be an alternative approach?

public var myProperty: Int from myStorage
private var myStorage: AtomicLazy<Int>()

The idea here is that implementations wanting access to the storage should declare and name a property that provides for another’s storage. Otherwise, the storage remains inaccessible:

public var myProperty: Int from AtomicLazy()

What I like about this is the developer can express that the overall implementation of the type with the property with delegated storage should (or does) not manipulate the storage directly.

2 Likes

I really love how your current iteration of the proposal ties property delegates with the attribute syntax and the potential custom attribute work. I think this is going in the right direction.

Concerning access control, shouldn’t the default be private instead of internal If those property delegates are implementation detail?

1 Like

I'd really love to see the access control aspect of this punted out of the proposal. Lazy properties (the only prior art that is similar) don't provide access to the underlying storage at all, and while people complain about it, this has not been a show stopper. I agree that this will be much more of an issue with this proposal, but I am still reticent to tackle this like you are.

Isn't it reasonable to just unconditionally give the backing storage private or perhaps fileprivate access? If you want to provide a .clear() method or something on your behavior, you can do so in the same extension that you define the property. If you want to provide access to the underlying storage, you can also define a forwarding property with whatever access control you want.

I acknowledge your point about that 'forwarding accessor' not being well sugared at the moment, but that seems like a possible future direction of the behavior feature direction, so I don't see why we would try to overly sugar this from the get-go.

-Chris

9 Likes

But wouldn't the key path be formed at compile time? Shouldn't that negate a lot of the cost.

It seems like passing self is necessary for a lot of popular use cases, it seems a shame to permanently limit ourselves to avoid the cost of passing a parameter.

But I can say the same of the current proposal. If we commit to value being a property instead of having a pair of getter/setter functions that can take parameters, it locks us into a design that permanently limits the design space.

I would argue that at the very least we need to take the self parameter, and that @hexdreamer's idea of taking self and a key path is the most future-proof design.

I was thinking about some use cases for property delegates which would require the delegate storage to be directly initialized in a class initializer (or in the case of a UIViewController subclass that uses storyboards using setter injection on an IUO after the instance is deserialized). I am wondering how the current pitch using attribute syntax would or would not be able to support these use cases.

Apologies for not reading the whole thread... 220+ replies is a lot to go through.

I don't like the by keyword, nor do I like using either.

I'm wondering if we can avoid the extra syntax by doing some sort of marker protocol. For example, what if we had a compiler-provided Proxy protocol, so I could do:

protocol Proxy {
    associatedtype ProxyType
}

struct Lazy<Value>: Proxy {
    typealias ProxyType = Value
    ... implementation here
}

Then we could teach the compiler to "unwrap" the Proxy automatically:

// `lazyInt` is of type `Int`, because that's the Proxy.ProxyType of the underlying type
var lazyInt: Int = Lazy(42)

Genericizing proxy types like this means that you could do things like implement Implicitly Unwrapped Optionals as a Proxy<Element>. It would also be useful for doing things like the Adapter pattern (and of course the Proxy pattern)...

Because

ie:

var lazyInt: Int = …

and not

var lazyInt: Lazy<Int> = …

I still think a simpler syntax off of var makes the most sense. I don't think this needs an entirely separate keyword.

var(Lazy) value: Int = ...
var(Atomic(1)) value

Is the desire for the by in the suggested position so to connect the possible type and delegate visually? I would think a delegate would be more associated with the declaration than the type.

1 Like

It's buried in the threads, but the proposal is shifting toward using custom attribute syntax rather than by or using. See my post here for my thoughts on this. Unfortunately, I haven't yet revamped the proposal to show this.

Doug

3 Likes

Aha, thank you!

I think there's a ton of potential in this idea. It seems, though, that we're dancing around a deeper concept. Delegating storage like this is a special case of the Proxy Pattern.

What if, instead of focusing on this being "property delegates", we instead made this as "can we improve the experience of implementing proxy types in Swift"?

In Objective-C, we've got NSProxy, which we can use to make any sort of proxy; the method forwarding mechanism means that for all intents and purposes, the NSProxy value we pass around is a value of type (whatever thing it's proxying).

Having support for proxies in Swift would be amazing; we could use it for proxying delegates, notification handlers, property values, etc.

5 Likes

I think properties are special enough that they'll need their own delegate/proxying semantics regardless of whether we have some over-arching framework for defining proxying types. I'm happy to talk about other forms of proxies in another thread, but I don't think it makes sense to widen the property-delegates discussion even further: it's already a sizable feature, and seems to be coalescing into something quite useful.

Doug

3 Likes

The attribute-based pitch (which I'm sorry I haven't been able to write up fully yet) still allows one to explicitly initialize the backing property, e.g.,

self.$foo = DelegateType(arg: 17)

Doug

1 Like

Awesome, so if I understand correctly, this assignment could happen in an initializer which would support some of those use cases.

Will it be possible to use an IUO backing property to support setter injection for storyboard-backed view controllers? Something like @DeleageType!?

The proposed design is based on an ad hoc protocol that delegates to a value property. There is no reason we couldn't extend the set of declarations to which we can delegate to include subscripts like the ones I described before, e.g., a delegate type could include a

subscript<InstanceSelf>(instanceSelf instanceSelf: InstanceSelf) -> Value {
  // ...
}

It's not all that different from the way in which SE-0252 extends the ad hoc protocol for the @dynamicMemberLookup protocol.

Doug

@Douglas_Gregor To what extent do you see property delegates as important to the user of a library? If the delegate type is internal only, would a library user know or care that there's a delegate at work?

I don't think it's important to expose the backing storage property publicly. We can simplify the proposal by not allowing one to specify the access of the backing storage; max(internal, access-of-original-property) is reasonable behavior.

Doug

1 Like

I don't understand the sort order being used. Is public greater or less than internal?

1 Like