Pitch: Property Delegates

I'm resisting because I don't want to introduce two ways to do the same thing without solid use cases for the second mechanism, because it complicates the description of the feature.

Doug

1 Like

Okay that's a fair argument. I want to ask you or in general the core team if you/they would think that it would be reasonable to allow non-generic or generic types with more type parameters as property delegates in the future?

Sorry if I'm being quite persistent on that one.

Speaking personally, it's a possible avenue for future extension, assume the design is good and supported by enough compelling use cases to motivate extension to the language.

Doug

1 Like

I have one other question that I'd like to ask. Assuming the proposal went though the review and is accepted. Will the stdlib extended with a set of (useful/common) property delegates (which the stdlib itself make use of)? In separate proposal of course.

2 Likes

This would make an excellent follow-on proposal.

Doug

3 Likes

I went ahead and prototyped support for using property delegates via attribute syntax. Essentially, this means that one would write:

@Lazy var x = 10

@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool

which is equivalent to the current proposal's

var x = 10 by Lazy

static var isFooFeatureEnabled: Bool
  by UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)

There are some restrictions that come with this design: we no longer have syntactic space for the by private / by public part of the feature. On the other hand, it dovetails with the discussion of custom attributes. I'll bring up the general idea over there.

Doug

15 Likes

This syntax looks really nice IMO. :-)

One concern I have with the attribute like syntax that it gives you an impression you could stack multiple property delegates together, which then likely will be diagnosed, but I can forsee that a lot of users will tap into that trap for a few times.

2 Likes

We'll have this problem with lazy, @NSCopying, @NSManaged, and other existing properties / modifiers already.

Doug

Yes please! This prefix syntax with "@" is so much better than postfix syntax using "by".

Prefix syntax is also more similar to how "lazy", "@IBOutlet" and others are currently used in Swift.

A possible idea for the private/public, even though it's not probably liked by everyone is to riff on private(set) so you would have private(UserDefault) used same way, i.e.

private(UserDefault) static var isFooFeatureEnabled: Bool

What about a slightly different formulation of the attribute approach that combines both concepts?

@storageBy(Lazy)
var x = 10
1 Like

As Doug already pointed out the issues remains with any attribute, while I think the originally pitched syntax limits the usage through the grammar to one delegate type. It's not a big deal though. ;)

That’s fair, but semantically, I think the restriction is better hinted at by this formulation since the attribute is “storageBy” not “Lazy”. I’d think people would be much less likely to think they could stack multiple @storageBy’s than one @Lazy and one @UserDefault.

2 Likes

I read the post Doug did in the custom attribute thread I personally I come to the conclusion that the attribute style of property delegates would be the ideal solution here, wether this is @Lazy, @storageby(Lazy) or @storage(by: Lazy) it does not really matter as it's in the end the readers preference.

If we would go down that road it leaves the syntax space for 'property forwarding' that we could tackle separately later, because not all properties are backed nearby the original property and not every storage is or can be a property delegate.

I like where this is going with custom attributes, but instead of treating every property delegate type a different attribute and having property delegates as a language feature, could this maybe be implemented as just another static attribute? Doug's custom static attribute suggestion seems to be a good enough abstraction to leverage for this.

A static attribute would be defined in the standard library that describes how the property is to be synthesized.

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

    var delegate: Delegate
    var delegateAccess: Access

    init(to delegate: Delegate, delegateAccess: Access) { ... }
    init(to delegate: Delegate.Type, delegateAccess: Access) { ... }
}

enum Access {
    case `public`
    case `internal`
    case `fileprivate`
    case `private`
    case hidden
}

PropertyDelegate would become a normal protocol. Users could then describe the property synthesis by applying the attribute:

class Test {

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

The Swift compiler would interpret the attribute. For the given example, it would synthesize the following:

class Test {

    public var $data: Lazy<Data>

    public var data: Data {
        get {
            return $data
        }
        set {
           $data = newValue
        }
    }
}

Delegated could be expanded with additional optional properties like delegateName if the user needs an alternative name, potentially not prefix with $.

The whole idea is very similar to ObjC way of synthesizing properties.

1 Like

What would the access level of the storage be? Identical to the property itself? Since we already have parameterized access levels perhaps we could piggyback on that with private(storage) or private(delegate). I think it's pretty important to have control over the access level.

Assuming we go in this direction for property delegates, the by <expression-or-key-path> syntax would be available if we want to some provide sugar for more advanced forwarding use cases. I wouldn't be surprised to see motivation for this arise as people start writing property delegates. In some cases they will discover they have a use case with requirements that go beyond the capabilities of property delegates. Forwarding is a way to provide about half of the usage-site syntactic sugar of property delegates. That means you don't face a steep cliff from all of the sugar to no sugar if a property delegate doesn't work out.

3 Likes

The access level for the storage property would be max(internal, access-of-declared-property), which is the default in the current proposal.

Perhaps. Without control over the access level, we'd have to fall back to manually implementing the property and its backing storage.

I think it's worth being clear that introducing the by syntax later will have a steeper cliff to climb, because it'll have fewer use cases and we'll have set the precedent of using attributes for property delegates.

But, yes, there is still syntactic space for adding forwarding and such later.

Doug

This is starting to get quite verbose (compare that to @Lazy or lazy).

Doug

3 Likes

This has been stewing in my mind for days now. Most of the argument has been about the spelling of this feature. From Java, I've seen how annotations can totally take over the front part of your declaration, obscuring the thing that you actually want to see, and becoming so verbose that it takes over multiple lines, destroying the actual flow of your code. (The annotation becomes the language.)

So how about something totally different?

A property behavior is pretty much exactly this:

var humbug = Behave<Int>(5)

So why don't we just call it that? The compiler could just know implicitly that, because Behave is a delegate property, every time you referred to humbug, you actually meant the wrapped value.

There's one problem with this, in that you're hiding this fact from the reader. So how about we change the "var" keyword to "via", as in you're getting this value via the wrapper?

via humbug = Behave<Int>(5)

This would also make it pretty clean to do multiple levels of wrapping, like

via humbug = Oh<Behave<Int>>(5)

An alternative spelling might be

var $humbug = Oh<Behave<Int>>(5)

This way, it's an extra signal that when you're talking to $humbug, it's the wrapper, but when you use just humbug, it's the actual value.

3 Likes