FWIW, this should fall out of the compiler 'walking' the custom attributes. When the new toolchain is available, it should be testable.
Doug
FWIW, this should fall out of the compiler 'walking' the custom attributes. When the new toolchain is available, it should be testable.
Doug
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
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
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.
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)?
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
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
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
?
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.
3 is seems particularly important to me. What if I want a lazy atomic, a thread local delayed mutable, a (delayed) immutable IBOutlet, etc.?
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:
@
will show you the @propertyDelegate
attribute. A web search will find relevant documentation, blog posts, examples, etc.@
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.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