Maybe I’ve missed where this is stated, but is adding a type constraint to the generic Property Delegate permitted and are there any additional restrictions?
Yes, that's permitted. I don't know of any additional restrictions.
Doug
I like the in
keyword better than by
. It mimics how you extract values from a collection in a for loop, except now you extract the value from a custom storage implementation.
To make things even better, I would rename the proposal "Property Storage", rename the attribute @propertyStorage
, and use the suffix "storage" to make them nice grammatical sentences:
var foo: Foo in LazyStorage
var foo: Foo in DelayedMutableStorage
var foo: Foo in CopyingStorage
var foo: Foo in CopyOnWriteStorage
var foo: Foo in ObservableStorage
var foo: Foo in SynchronizedStorage
var foo: Foo in IndirectStorage
var foo: Foo in UnsafeMutablePointer
var foo: Foo in UserDefault
var foo: Foo in DatabaseColumn
var foo: Foo in XPC
var foo: Foo in Box
We could use a capture list after the declaration. Variables would be captured on access and passed to the subscript:
struct S {
var mutex: Mutex
var foo: Foo by Synchronized() [mutex]
var bar: Bar by Synchronized() [mutex]
}
For a class, you'd be allowed to capture self
because there is no exclusivity rule that apply:
class C {
var foo: Foo by Synchronized() [self]
var bar: Bar by Synchronized() [self]
}
And perhaps self
could even be implied in cases like this.
I did a quick read-through of the proposal, and only skimmed some of the responses, but I'm concerned that this proposal and its example Lazy
type cannot possibly replicate the built-in lazy
. In particular, a lazy
property can reference self
(without producing a retain cycle). This proposal does not allow Lazy
to do that, because it would be capturing self
before self
is fully initialized, but if it did allow that then it would produce a retain cycle (for classes, or be simply broken for value types).
For example:
lazy var foo: String = self.calculateFoo()
This is perfectly legal today. With this proposal, var foo by Lazy<String> = self.calculateFoo()
would not.
Delegates have a number of features that apply only to them:
- They create a parallel
$
property that users sometimes need to use and control access to. - They are mutually exclusive with all other delegates, but not generally with other attributes. (Ordinary attributes have idiosyncratic conflicts between each other, but are generally considered compatible until proven incompatible.)
- They correspond to actual types with documentation you can read.
- They need to be applied to a single declaration, not a complicated pattern match.
- They prevent you from declaring accessors.
The $
point is especially important—people need to know it's there. A unique syntax would make sure that they do.
I have read this pitch and giving a large +1 to it.
The way I understand the idea, is that property delegates are instead backing the property with a custom storage. I’m not even entirely sure if delegate is the clearest way of describing it (though I trust the authors of the pitch to have picked the best name)
If this feature continues down that path, I have hard time seeing how a property can be backed by two storages at once.
If one wanted composable traits, or behaviors, then I think it would be a very different kind of pitch, more like SE-0030.
Now just “thinking out loud”, how about looking towards configurable backing storage types instead? I understand that we likely don’t want to go down the path of one storage type with all kinds of bools on it (isCopyable, isCOW, etc) but maybe there’s something there that could make sense, once technical possibility of having that is in place, which is what is proposed in this pitch.
And to join in on the bikeshedding of by:
var x: Int backedBy LazyStorage
would make most sense imo.
Adding to the list: they can have access modifiers applied to them. One of the reasons I really like your idea of attribute-like syntax, but with a $
prefix on the delegate name is that I think when people learn the feature the $
will pop out and help them remember that the syntactically invisible backing storage property is also present.
Most of these are not unique to delegates vs. other modifiers/attributes, and it's notable that we have multiple features that are essentially built-in property delegates (with slightly stronger/different rules) that are currently spelled with modifiers and attributes. So using a different spelling for these is actively rejecting four years of precedent for similar features.
The only thing that's really different here is that we're also declaring the delegate-property name, which means that we want to modify that declaration with things like access control (and @usableFromInline
?). It's not obvious to me that that's sufficient to justify adding a new syntax, though, when we can surely find some way to combine that with attributes.
Really like the direction of this proposal, thanks to all involved for the hard work.
Is it right to think the property delegate is treated by the compiler as a generic protocol?
One thought that occurs to me is why the property delegate attribute is required since a protocol treated differently by the compiler as suggested in posts above seems like it should work just as well?
Another thought: having an invisible storage property declared feels weird since the code reads like I’m declaring one property but in fact I’m declaring two. Is there a way to avoid it? For example:
var data: Data by Lazy = ...
var storage = data as Lazy<Data>
storage.reset()
Isn't the example you just gave of modifying the delegate with attributes a good reason not to use attribute syntax for declaring the delegate itself? It seems to me that adding attributes like @usableFromInline
to a property delegate itself declared with attribute syntax could easily be confusing. I think there would be much less potential for confusion if a different syntax is used.
Well, the idea would presumably be something like
@Lazy(public @usableFromInline, closure: "hello")
@usableFromInline
public var name: String
which does not stand out to me as notably worse or more confusing than
@usableFromInline
public var name: String by public @usableFromInline Lazy(closure: "hello")
I agree, but I'm not a fan of the by
syntax, especially because it places the delegate after the type instead of in modifier location. I also agree that the syntax you posted isn't confusing regarding which declaration an attribute applies to. However, I don't like it because it loses the direct syntactic correlation with invoking the initializer of the delegate type.
I agree that losing the connection with just initializing the delegate type is unfortunate. On the other hand, I don't see how you would solve that problem with $Lazy
any better, and any solution you invented for the one would seem to equally benefit the other. It's a real advantage (the only advantage in my opinion) of the by
-style syntax that it does naturally allow you to pile up modifiers on the delegate.
I think at some point it's also reasonable to say "no, just use a computed property instead". But that would argue for allowing properties named $foo
directly (perhaps in backticks).
There's a pretty huge jump in boilerplate to use a computed property. Maybe if we had property aliases.
…I was waffling over whether to include this, but
@usableFromInline
var `$foo`: Lazy<Int> = .init(initialValue: 17)
public var foo: Int by $foo
"If the lookup finds another property whose type is a behavior, you can delegate to that too."
But I don't know if we need that in the initial proposal.
If we wanted to do that, it would definitely be a better justification for the by
syntax. But I don't actually we do; I think it'd obviously be better to just add a general property-alias declaration that could do arbitrary paths from self
:
public varalias foo: Foo = $foo.value
public varalias name: String = personalities.first.name
(or alias var
rather than a new keyword if you like)
Ok, here's a rough sketch of how we might use generics to support composition. This design requires using protocols rather than an attribute.
A marker protocol WrappablePropertyDelegate: PropertyDelegate
would be used to mark delegates which are safe to use in the inner layer of a composition.
Types conforming to PropertyDelegate
which are safe to use in the outer layer of a composition (i.e. higher-order delegates) would constrain their type argument with WrappablePropertyDelegate
. These delegates would use the wrapped delegate for their internal storage. These delegates must also use a Wrapped.Value == Value
constraint. Additional constraints would still be supported, including other protocols that refine PropertyDelegate
. One of the potentially large benefits of this approach to composition is that refining protocols can be used to provide semantic constraints intended to ensure the wrapped delegates are compatible with the wrapping type.
// this example intentionally uses meaningless type names in order to avoid
// discussion of whether a specific composition is a good idea or not
struct InnerDelegate: WrappablePropertyDelegate { ... }
struct OuterDelegate <Wrapped: WrappablePropertyDelegate>: PropertyDelegate {
typealias Value = Wrapped.Value
}
// normal generic syntax is used for composition
$OuterDelegate<InnerDelegate>
var foo: Int
// when the generic parameters are omitted the compiler inserts an appropriate identity delegate type
$WrappingDelegate
var foo: Int
// types that conform to `WrappablePropertyDelegate` *and* constrain their own
// type argument to `WrappablePropertyDelegate` can play either role
// or even *both* roles in the same composition, such as `InnerAndOuterDelegate` below:
$OuterDelegate<InnerAndOuterDelegate<InnerDelegate>>
var foo: Int
Because normal generic syntax is used for composition we retain the syntactic relationship with the delegate's type name, and with initializer invocation when the delegate is initialized directly.
I am showing a sketch of the protocols necessary to support this approach below. I omitted dealing with mutating get
and nonmutating set
for now as they add complexity and aren't essential to communicating how this design would work. There is plenty of room for discussion in this design, especially around any builtin magic that might be appropriate and help to streamline it.
The basic protocols to support the present pitch would be something along the lines of:
protocol PropertyDelegate {
associatedtype Value
// It may be that the `value` property needs to move to refining protocols
// to handle `mutating get` and `nonmutating set`.
var value: Value { get }
}
protocol MutablePropertyDelegate: PropertyDelegate {
associatedtype Value
var value: Value { get set }
}
protocol InitializablePropertyDelegate: PropertyDelegate {
associatedtype Value
init(initialValue: Value)
}
protocol AutoclosureInitializablePropertyDelegate: PropertyDelegate {
associatedtype Value
init(initialValue: @autoclosure () -> Value)
}
InitializablePropertyDelegate
and AutoclosureInitializablePropertyDelegate
probably need to be mutually exclusive so the compiler doesn't have to prefer one over the other when a delegated property is assigned. Or perhaps we can use builtin magic to collapse these to a single protocol that allows the conformer to choose to use @autoclosure
or not.
Composition would be supported as follows:
protocol WrappablePropertyDelegate: PropertyDelegate {}
// We could possibly use a marker protocol for the outer layer as well although it is probably not necessary.
// One capability (of questionable value) that would be gained by this approach would be the
// ability for a delegate to constrain its type argument to `WrappablePropertyDelegate`
// *without* itself being usable in the outer layer of a composition.
protocol WrappingPropertyDelegate: PropertyDelegate {
associatedtype Value
associatedtype Wrapped: PropertyDelegate where Wrapped.Value == Value
}
I'll end this with an example showing how conditional conformance can be used to by higher-order delegates to conditionally support direct initialization depending on their underlying delegate:
extension MyWrappingDelegate: InitializablePropertyDelegate where Wrapped: InitializablePropertyDelegate {
init(initialValue: Value) {
wrapped = Wrapped(initialValue: initialValue)
}
}
extension MyWrappingDelegate: InitializablePropertyDelegate where Wrapped: InitializablePropertyDelegate {
init(initialValue: @autoclosure () -> Value)
wrapped = Wrapped(initialValue: initialValue())
}
}
- Any use of protocols probably makes the entire feature deployment-target-limited, which would be a huge shame.
- I guess we'd need four different protocols to handle various combinations of
mutating get
/nonmutating set
? And those don't really compose. - I think being able to customize the implementation of a composition is probably a requirement, but I don't think that's possible here.