How about foo.$.directAccessToPropertyDelegateForFooAkaStorage()?
Thank @Jean-Daniel, @calebkleveter and @Douglas_Gregor for the explanation. I was just curious about the reason of the convention and now I understand the reason behind it.
I've read through the new pitch, and I've got some scattered thoughts on this.
- I like the idea of "unifying" of property storage behavior principles, but I'm not sold.
- If we're not going to get
private(storage)(or equivalent) during the initial implementation, then the storage property should beprivate(maybefileprivate). That way the code author has as much control over exposing implementation details as they can possibly have. If they really need to, they can re-surface the underlying storage themselves using a new computed property. -
MutableCopying<Value: NSMutableCopying>, anyone? - One thing that wasn't clear to me: Will the examples listed in the pitch (
@Lazy,@Atomic,@Copying,@UserDefault, etc) etc be provided by the standard library? Or is this going to be the Wild West of property delegates where@Lazymeans something different in one project than another, because those projects have differingLazy<Value>implementations? If they will be provided by the stdlib, can the pitch please include an explicit list of the behaviors that will be included? - It seems natural to want extend this to include all storage qualifiers like
strong,unowned, andweak; since we're already talking aboutlazyvs@Lazy, how about extending this to@Unownedor@Weak? - I'd like to see more about compositing storage behaviors. I can think of places where I've implemented the equivalent of
AtomicAndLazy<Value>; Will that be my responsibility to write? And then again, will I have to deal with almost-but-not-quite-the-same implementations across disparate codebases? (Or was this thatstorageValuething that I didn't really understand?) Or can we think of a way to get better built-in support for storage composition? - The inability to make
PropertyDelegatea protocol because of the mutating/nonmutating stuff seems like a major shortcoming in the protocol features of Swift. As I think about it, I believe that the composition of behaviors would work better if the behaviors could be described as conforming to a protocol. - Suggested name alternatives to Behavior/Delegate:
PropertyStorage,PropertyMonad,PropertyQualifier
I might have more thoughts as I mull on this, but this is what I've got for now.
I asked this before in the original thread and Douglas said that this should be a follow up proposal if this feature is accepted.
I'm finding myself feeling the same way. When I see variable attributes (@objc, @IBOutlet, etc) I mentally parse the @ as part of the keyword, not as a prefix or an operator. It's also a signal to me that some kind of special-case compiler behavior is probably being invoked.
Using that same @ syntax to refer to a user-defined type name feels a little awkward. In a declaration like @Lazy var foo = 123, the attribute @Lazy looks more like a keyword than a reference to a type Lazy. But I guess that's more of a comment on the custom attributes syntax than this proposal specifically.
Will it be possible to use multiple delegates?
With structs we can write something like Lazy<AnotherDelegate<YetAnother<Int>>>; can we compose attributes like this?
Yes, but only if the outer layers are specifically designed to support this. I sketched out an approach to higher-order delegates in the pitch thread. One downside is that this approach requires an Identity delegate if you want to be able to use a higher-order delegate without an inner delegate. It also isnβt clear how it will work with inference of the underlying value type (if anyone pulls the toolchain and tries it please let us know!).
This proposal is generally looking really great to me (in a quick scan), I love the integration with attribute syntax, I love love that you deferred access control extensions to a future proposal. I'm still not thrilled with the $x syntax for identifiers (did you consider using underscore?) but overall, I'm super excited about this coming together!
I have two thoughts after reading Pitch #2:
-
I really worry about the
@Typesyntax, because there's absolutely nothing at the point of use to suggest that you can't stack@Copying,@Atomic,@Whatever, and whack-a-mole errors are not fun for anyone. I can see this becoming a truly horrible frustration for users of these features, since it will never be clear which@Thingsare actually@propertyDelegatesand which are some other kind of custom attribute which can stack. Since you really can only pick one of these for any given variable, was there any further thought given to something along the lines of@delegatingTo(Atomic)(or@storage, or@foobar...)? -
It seems that the argument for greater-than-private access to the storage is specific to certain kinds of property delegates (ie:
@Atomic). Might we consider hanging the accessibility in the type declarations somehow? I get that there are reasons for the storage of an atomic property to be accessible, but there are equally strong reasons for the underlying storage of a lazy property (or copying, or delayed mutable, etc) to not be accessible outside the type (or at all). It would be a real shame to move forward with a "guts-out" approach for the handful of types that do need it, but hamstring the safety of the types that really want to be fully encapsulated.
Maybe something along the lines of @delegatingTo(Copyable, private) or @delegatingTo(Atomic, public)... ?
Thanks for the thread reset and revised pitch. It's very helpful to see the current state summed up. I've read a lot of the existing discussion but it's still hard to keep the overall picture in mind.
This is a lovely and very thoroughly worked out pitch. It would be fine as it is, although I have some additional feedback. My comments are for the most part bikeshed-level except for the issue of self of the enclosing type being accessible to the delegate. The multiple restrictions, special cases, and caveats around this issue in this proposal are, I think, pointing to a gap in the general treatment of self during initialization. Yes, this is indeed a separate language issue, but it seems pretty directly related and this proposal shows a clear motivating case.
Many of the potential uses of self in the init context are perfectly benign, at least for reference types. For example, you often want to initialize a member with a callback closure that references self. The tight restrictions in current Swift are partially helpful and partially just a reflection of the compiler's inability to guarantee that self won't be abused if it escapes from the init environment.
I'd really like to see an attribute analogous to @escaping that promises "I will record this closure or argument but will not run/access it as a direct result of this call." If it were legal to include self references in those specially-marked APIs at init time, that might simplify the initialization of objects that used callback-based APIs and might facilitate some of the obvious applications of property delegates, such as a direct replacement for lazy.
I believe the compiler already treats initialized members as being untouchable until the "safe point" of initialization, so would this scheme not automatically maintain init-time safety while also allowing more access to benign uses of self?
Minor nits and comments are below. I think some of these have come up before during the previous discussion threads, so please excuse my lack of proper referencing.
-
This is, again, a larger issue, but most existing attributes are so magical that it seems a little sketchy to me to mix user-defined attributes in with the grab-bag of random stuff provided by the compiler. I'd rather see
@delegate(Lazy)than@Lazy(or in a more complex case,@delegate<Int>(Lazy, resetValue: Int)). I think@delegate(Lazy)is clearer, too. The@delegatepart is the compiler magic, and theLazypart is the user-supplied implementation. The@delegatepart never changes, which is appropriate since the magic shared by all@delegateproperties is identical; they vary only in the user-supplied implementation. The use of a common@delegatenotation also clues you in that multiple delegate specifications will conflict. -
@propertyDelegateas a name seems inapposite since the entities whose implementation is being delegated are not necessarily properties; they could be any variable. If@delegate(Lazy)was the spelling for the variable, the marker for the implementation could be just@delegateor maybe@delegateImplementation. -
I'm not sure I understand why it's of value to employ a naming convention (
$foo) that was formerly unavailable to variables. If you were to implement these things by hand, you would by tradition use_fooas the backup variable. Why is that not the go-to convention here? There's no possibility of confusion or ambiguity; you pick the variable name, and all you have to do to avoid trouble is not also use the name_name. And furthermore, all code using these delegates will be 100% new; why would it even occur to someone to stomp on_name? Even if you did it, you'd just get a compiler error, right?$variableis better reserved for some context in which its guaranteed availability is genuinely useful. -
That said, I really see the backing variable as being an element or property of the variable being defined.
name.delegatewould be ideal, (e.g.,myVar.delegate.reset()) but of course that's off the table. However, the form feels right to me. If there's going to be a magic name, just make it the same name for all delegated variables and put it in the namespace of the top-level variable. Yes, there's a small possibility of collision with an existing API, but collisions are easy to work around: just extend the underlying type to add an additional name for the now-conflicting element. -
Am I correct in interpreting the pitch to say that all delegated variables must have setters? If so, why is that, exactly? I've often wanted
lazy let, for example.
Again: just nits and future directions. The pitch looks very solid.
An alternative to the $ notation can be to use key paths.
If we define a new protocol for key paths that are assocated to property delegates:
protocol PropertyKeyPath {
associatedtype Delegate
}
We can make a new subscript that takes a PropertyKeyPath and returns the associated delegate:
self[storageOf: \.property]
Edit: the code was wrong
Doesn't that assume that all properties will have the same delegate type?
I'm assuming that a key path like \Object.property will have a type of its own, conforming to KeyPath. And if the property field has a property delegate, the type will also conform to PropertyKeyPath with the proper delegate type.
Right, but what's the declaration of the the [storageOf:] subscript? What type does it return?
If returns the delegate of the property, here is the signature:
public subscript<T: PropertyKeyPath>(storageOf keyPath: T) -> T.Delegate
So if a property is declared as @Lazy var foo, then I assume that the key path \Bar.foo will automatically conform to procotol PropertyKeyPath, with Delegate equal to Lazy. And then if someone wants to access the storage, they will write self[storageOf: \.foo].
You imho just found the second reason not to use an underscore ;-):
Afair, $storage is not allowed as a property name, so there can't be any collision.
Could you spell out your concern with name collisions? My thinking was, if you had an existing lazy implementation, like this example from the proposal:
struct Foo {
// lazy var foo = 1738
private var _foo: Int?
var foo: Int {
get {
if let value = _foo { return value }
let initialValue = 1738
_foo = initialValue
return initialValue
}
set {
_foo = newValue
}
}
}
and you wanted to update it with the new property delegate feature like so:
@Lazy var foo: Int = 47
you'd want to remove the now-unused private var _foo declaration anyway, as part of the code rewrite. I'd imagine that if you didn't, you'd receive a compiler warning, ie "Explicit property storage declarations for @Lazy must be of type Lazy<Int>", or some such. Does that make sense?
Is there a situation where you wouldn't be able to remove a var _foo declaration, and thus would experience a name collision? Or did you mean there's a collision in a more general sense, as in a _ could mean either a user-declared variable or a compiler-synthesized variable?
(Also, what's the first reason not to use an underscore?
)
I kinda like the $foo names as a reminder that it's compiler-synthesized, and my mind considers it more tightly-coupled to the original variable when I see it in a function body. But I could live with _foo. (It does seem weird if we ever allow making the delegate public, though.)
The property behaviors proposal went a little way down this road. I think these are ideas worth pursuing, but it's a big jump in complexity for the model and I'm concerned we won't converge on a design (based on what happened with property behaviors).
Doug
We'd need to have a way to make the @IBOutlet replacement infer @objc, and yeah, it feels like DelayedMutable would be a good model for it... maybe with a nicer failure diagnostic about "did you forget to connect to this outlet?". I haven't thought about it beyond that.
I was planning to expose the name of the delegate used (so you'd see @Lazy var foo: Int), but that's not strictly necessary.
Yup.
Okay, thanks!
Doug