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())
}
}