SE-0258: Property Delegates

FWIW, this should fall out of the compiler 'walking' the custom attributes. When the new toolchain is available, it should be testable.

Doug

1 Like

Apologies for repeating myself, but we can make the delegate members visible via autocomplete and go back to insert the '$' in the right place.

Doug

FWIW, before posting my original pitch, I went through and prototyped a design that involved a postfix operator that could be repeated to expose different 'levels' of the composition. People found it too clever, and one problem with postfix operators is that they're actually less discoverable that (say) a separate property. You can pull the same tricks I mentioned before where you provide the API of the delegate type on the original property in code completion (inserting the $ where appropriate), but having a desugaring to a separate property is a simpler overall mental model. "This syntax with the @Foo desugars to ."

Doug

I consider this a tooling problem, where Cmd-Click on an attribute should give you documentation about what it does. Amusingly, with custom attributes, we'll get this behavior for free because custom attributes refer to types---which already have all of this infrastructure. Documentation for @Lazy would be easy to find that for lazy.

Doug

This is supported; see the UserDefaults example and this section.

I don't agree with this. The delegate type can control its API via the normal access control rules. It shouldn't have different API when you have a "normal" instance of the type vs. when it's a delegate for another type, because that breaks the "it's just a type" model.

Doug

2 Likes

I think this would be a nice improvement to the language that composes well with property delegates. It stands alone for, e.g. something like:

let d: Dictionary<_, Double> = ["one": 1, "pi": 3.14159]   // infer the Key type to be String

I don't know that we could intercept the reference-counting operations well enough, but it's possible that we could pull some Builtin tricks in the standard library to do this.

Doug

2 Likes

I haven't had time to write a full review for this proposal but I'm fully behind it. I'd just like to point out that for certain use cases, it's very important for the backing storage to be synthesised as a normal property. It allows it to be discoverable at runtime through Mirror, greatly improving its usefulness. For example, the snippet I posted to Twitter depends on it being accessible.

1 Like

Is there any chance that these could be used on function arguments? @CalledOnlyOnce could be implemented this way. Compile time warnings would be better butā€¦

Ya, the reference storage type question was mostly to probe the potential of the proposal and possible future directions for the reference storage types. Thanks.

That direction was my suggestion prior to the introduction of delegateValue. It solved the access control issue fine in that design. Itā€™s less clear to me in the delegateValue design because we havenā€™t identified a solution for out-of-line initialization when the type of delegateValue is different than the type of the property delegate itself.

If we find a solution for out-of-line initialization it might or might not be sufficient. For example, we could say $$foo is used for out-of-line initialization only and is therefore always private and possibly not even available outside of initialization. If we do that then a single additional modifier for $foo would be sufficient but parameterizing it with storage might be misleading since the modified value isnā€™t always the storage. Unfortunately delegate would be just as misleading. We would need to use the rather verbose delegateValue in order to be 100% clear.

Good to hear that there is a better option. I donā€™t think the proposal needs to depend on an experimental feature. What I am looking for is motivation for delegateValue and I donā€™t see a compelling motivation in the examples in the proposal. I think itā€™s an interesting feature but it introduces complexity and edge cases into the design. It should be well motivated.

You did reply to me and I let it drop in the discussion thread. But as I continue to think about it I really donā€™t see how directly exposing the box makes the design better. As far as I can tell, unless there is a reason users need to access a Box directly there is no reason for it to be part of the user-facing API.

Since your design hides the Box altogether it isnā€™t at all clear to me what advantage there is for users to have to know to use @Box for a delegate that is directly initialized with a value instead of just using @Ref everywhere. It could be that this is just a difference of opinion. But if that is the case it is not a strong motivating example for delegateValue. A stronger motivating example would provide a use case that is unambiguously better off with the use of the feature.

Please donā€™t misunderstand: Iā€™m not trying to argue against the feature in principle. I just think we need to understand what the use cases are better than we do in order to be confident that we have the right design. It introduces edge cases Iā€™m not entirely happy with. This makes me wonder if it is the right approach or if there might be alternatives that come to mind when we better understand the problems weā€™re trying to solve with it.

Even if it were implemented, this solution still only covers some additional use cases. It would not provide out-of-line initialization when the delegateValue is not of type Self - as with @Box for example.

Thatā€™s fair. However, itā€™s worth pointing out that the introduction of delegateValue introduced its own kind of ā€œweirdnessā€ (which is probably why it wasnā€™t clear to me on the first reading). A property delegate is no longer a straightforward backing storage property of the kind we can write manually today. It is able to provide a stand-in for itself.

Good to know!

Right, but that still leaves us with delegates that donā€™t support out-of-line initialization.

One final question: what prompted you to add delegateValue to the second draft of the proposal? There must have been a reason but I havenā€™t heard it stated clearly. Why was this feature selected for implementation now instead of making it a possible future direction (and instead of designing and implementing one of the other future directions that have more clearly understood use cases)?

1 Like

A huge +1 from me! I like the @ syntax. I like the $ syntax. It will enable many patterns we can't yet imagine. I think the feature is easy to understand as it all simply comes down to plain Swift types, which we all understand.

I also hope follow-on proposals for access control and access to self and the property's keyPath will follow soon.

Yes, absolutely. It will remove so much boilerplate code as we will learn to put this feature to good use.

Yes, it does. In my opinion, this is the first proposal that enables macro-like functionality in Swift. So, naturally, some new syntax like @Lazy and $property has to be introduced. This may feel unfamiliar and therefore not 'swifty' to some but I absolutely think this is the right direction.

I haven't used a feature like this in another language.

I have followed both pitch threads pretty closely and have read multiple versions of the proposal.

We could extend the model to function parameters, but we have to decide what the caller and ABI do. Does the caller always provide a value of the delegate type, or do we go through the init(initialValue:) when itā€™s available? Is the ABI in terms of the delegate type or the stated parameter type?

Doug

2 Likes

My initial reaction is that the caller should always provide a value of the wrapped type and you go through init(initialValue:). The question in my mind is whether init(initialValue) runs on the caller side (ABI in terms of the delegate) or is synthesized at the top of the function body (ABI in terms of the initial value). Maybe Iā€™m just stating the same question in a different way...

Is there an advantage of leaving ABI in terms of the wrapped type? It seems like this would allow the delegate to be changed without breaking ABI - it becomes an implementation detail. Callers donā€™t have a way to interact directly with the delegate so there probably isnā€™t an advantage of exposing the delegate type in the ABI, is there?

Probably not. Delegate types aren't part of the public interface or ABI for properties.

inout becomes really interesting here, although presumably it's fine... we put the value into the delegate type, then write it back at the end.

Don't forget that there are delegate types without an init(initialValue:); those would presumably have to pass via the delegate type.

Doug

There are conceptually three things going on with

@Box var foo: Int

(1) the actual storage for the value (a Box), (2) the value as you see it (produced by value, accessed by foo), and (3) an abstract reference to a value (produced by delegateValue, accessed by $foo). There could be different storage strategies for (1) with a common abstract-referencing scheme via (3). The ability to use key path member lookup on (3) is really nice from this abstract-reference approach.

Eh, sorry. I really wanted to get something out for discussion that was implemented enough for people to poke at it, and I overestimated how much time it would take to get this bit into shape (and underestimated how much heartburn it would cause as I evolved the proposal). I find the Ref / Box thing very interesting because it allows us abstract away the storage completely, and it was the last bit of "control" that I felt like delegate type authors needed to craft their APIs. For example, you could literally have an empty storage type and use Mirror (as @hartbit notes) to figure out what properties were declared.

Personally, I'm happy with the private $$foo approach here. We're way out in the margins where you don't want direct initialization and have a delegateValue.

Doug

2 Likes

Or maybe just invalid as parameter delegates. What is the value of using a delegate if callers have to provide a value of the delegate type? All a parameter delegate would do is immediately wrap an argument, prevent the body of the function from using the direct argument value, and allow the implementation to avoid saying .value manually. Delegates that donā€™t support init(initialValue:) seem of dubious value in the parameter context.

The evolution processes of other languages and projects require some attempt at documentation and learnability ā€” even just one-liner headerdocs. Apple's own API review processes do as well. I have to feel like creating a one-off construct that can't have documentation added to it according to current language rules and punting on how that gets settled would be frowned upon in those communities.

The Swift community shouldn't have to beg to get bare minimum documentation.

If the proposal were accepted as-is, and continued to spell its main feature addition as a mystery meat attribute, the UX for a developer who hasn't already read this proposal looks pretty unpleasant. How do I know this feature exists in the first place? Maybe I'll monkey-see-monkey-do everything based on @Lazy, assuming I find it in the first place. How would I ever know about the different forms of init(initialValue:), or whatever we come to do about subscripting with self?

2 Likes

Yeah, I can see the moving parts. It just isn't clear to me what benefit users get from (1) being a visible part of the API surface. Can you elaborate on what the different storage strategies might be and why users would want control over that? It might help motivate this capability better.

I find the feature and the ability to abstract away storage interesting. And I don't doubt that it will be useful in some way. I just feel like it might be good to understand more concrete use cases in hand before committing to a design. As with other features, there is no harm in making it a future direction if we have uncertainty about this part of the proposal.

Is there a post by @hartbit that goes into more detail on this? This sounds interesting but the details are pretty murky (for me anyway).

So $$foo would always be synthesized as private for properties with a delegate that implements delegateValue? Or would this identifier be limited to initialization only?

This approach certainly plugs the hole in capabilities. Maybe it's the best solution. I just don't feel confident in that yet.

Boy, I'm not a really fan of how the @attribute syntax stuck. It started as implementation details/hacks, and now it's creeping into ever more domains (dynamic calculability, inalienability, property delegation, and more that I don't even know about, I can't keep up).

Before we embark this property delegate route (which I'm really excited about!), I think we need to take a second and define and clean up the details of attributes.

  1. How are they spelled?
  2. How are declarations with multiple attributes spelled?
  3. How do multiple declarations compose?
  4. Can attributes be de-magic-ified, and exposed as simple protocols (perhaps with some intrinsic magic types where necessary, but otherwise generally available for end user user), Rust/Java style?
  5. What runtime capabilities do they offer?
  6. What "hooks" do they offer, that users can plug into?

3 is seems particularly important to me. What if I want a lazy atomic, a thread local delayed mutable, a (delayed) immutable IBOutlet, etc.?

3 Likes

Your hyperbole detracts from your message, but I'll nonetheless try to answer.

Proposals serve as the documentation while we're going through the evolution process. Some time after acceptance, @propertyDelegate will be documented in the TSPL with other attributes. That will be augmented by blog posts elsewhere, more fleshed-out examples, and wider adoption of the feature that will bring more awareness. This is how I learn about features in any language I poke around with. If you find this approach inadequate, start another thread with constructive ideas to improve the situation.

Discoverability is straightforward:

  • Code completion after @ will show you the @propertyDelegate attribute. A web search will find relevant documentation, blog posts, examples, etc.
  • Code completion after @ will show you property delegate types you can choose from. Try one and see what it does. Their definitions will be clearly marked with @propertyDelegate; web search that to find more information.
  • QuickHelp and Jump to Definition will work on any use of property delegates you see in your code. The documentation comments will show you how to use that specific property delegate.

This really isn't different from any other part of the language, so either you have a wildly different understanding of this feature's obvious tooling support than I do, or you have general complaints about Swift's documentation and tooling that belong in a separate thread.

Doug

3 Likes