Okay I put together a comparison of the currently proposed features and how I personally would approach the feature:
In short:
- I would like more manual control over property delegates where the synthetization should be opt-in.
- Instead of requiring an initial value for
Value
on the RHS of the=
operator we should require the initialization of our property delegate. (e.g.var property: Value by Delegate = Delage( /* anything not just 'Value' */ )
- We should simply move all keywords which would apply near the
$
prefixed identifier of a delegate property after theby
keyword. That completely copies the same behavior from the manual declaration to the synthetized one. (e.g.public var property: Value by /* implicitly internal*/ private(set) lazy Delegate = Delegete(...)
- Exported API should show the
$
prefixed identifiers instead of the computed property withby
declaration.
Here is the gist (I'll update it if necessary):
Here is same content from the gist:
Property Delegates
The original proposal: https://github.com/DougGregor/swift-evolution/blob/property-delegates/proposals/NNNN-property-delegates.md
What is proposed?
- A user can create custom property delegates that allow a
var
declaration to decide how to implement the storage. - The original proposal want to use a new identifier space, prefixed by
$
, to uniquely identify a property delegate from the computed and non-prefixed main property. - To bind a property delegate to a
var
property one would need to use a new syntax that tells the compiler to 'synthesize' the delegate itself and theget/set
of the computed property routed to the delegate. - [Personal observation] Automatic synthetization brings some limitations:
- Access to
self
of the delegate container type is not possible. - Initialization is limited to predefined set of rules.
- Access to
- A user is only allowed to operate on the
$
prefixed identifiers but not declare any custom ones. - The previously mentioned syntax is expressed as
var foo by Delegate = ...
orvar foo: Int by Delegate = ...
. - The delegate is always exposed to the user unless it is explicitly prefixed with an access modifier (e.g.
var foo: Int by private Delegate = ...
- The delegate type must by a generic type with a single generic type parameter for the inner
value
property.
What I would reconsider
Disclaimer: this is only "my personal opinion"!
To begin with I would avoid the automatic synthetization of the property delegates, but keep the synthetization for the get/set
of the main computed property. We should also simplify the requirement for @propertyDelegate
attribute:
-
A property delegate must contain a property named
value
. -
The
value
's type will be used by the compiler to match the type with the property the delegate is attached to.struct Delegate { typealias Value = X var value: Value { ... } } [...] var $identifier: Delegate { ... } var identifier: X // synthetized get (& set)
-
The delegate type is not required to be generic, but it must expose
value
and its type at least with a type alias to the same access level as the delegate itself.public struct Delegate { typealias Value = X // error: typealias is 'internal' but must be 'public' var value: Value { ... } // error: property is 'internal' but must be 'public' }
-
Make the
typealias
required until generic types in Swift would expose the generic type parameters to 'qualified lookup' (pitch: Allow Member Lookup to Find Generic Parameters) -
It should be possible to nest property delegates to allow re-usage. To avoid extra properties one can make
value
delegate to a different delegate.@propertyDelegate struct SynchronizedLazy<T> { typealias Value = T var $value: Synchronized<Lazy<Value>> init(initialValue: @escaping @autoclosure () -> Value) { $value = Synchronized(initialValue: Lazy(initialValue: initialValue())) } var value: Value // synthetized get (& set) }
-
A property delegate can have any access level as other types in the language.
Compared to the original proposal I suggest that the user should be able to declare custom properties that are prefixed by $
. However a $
prefixed property is restricted to types that are marked with the @propertyDelegate
attribute.
-
When the user declares a custom
$
property the compiler will also require a computed property with the same identifier but without the$
prefix.var $foo: Delegate { ... } // error: computed property 'foo' is missing var $bar: Delegate { ... } // okay - `Delegate.Value` matches with the type from `bar` var bar: X // synthetized get (& set)
-
A delegate property can be marked as
lazy
.lazy var $baz: Delegate = Delegate(...) // okay var baz: Delegate.Value { ... }
-
A user can manually initialize a property delegate like any other types in the language.
var $baz: Delegate var baz: Delegate.Value { ... } init(value: Delegate.Value) { self.$baz = Delegate(value) }
-
The user has two choices on how the computed property should behave:
-
It should be possible to omit the computed property
{ ... }
body, which will tell the compiler to look for a property delegate that has the same identifier but prefixed with a$
, match the type with the type fromvalue
, and infer whether the computed property should only synthetizeget
or bothget
andset
.struct Delegate1 { typealias Value = X var value: Value { get { ... } } } struct Delegate2 { typealias Value = Y var value: Value { get { ... } set { ... } } } var $id_1 = Delegate1(...) var id_1: Delegate1.Value // synthetized get var $id_2 = Delegate2(...) var id_2: Delegate2.Value // synthetized get & set
-
The user can partly or completely opt-out of the automatic synthetization (like for
Equatable
for example). It should be even possible to provide aset
while the property delegate'svalue
isget
only.var $id_3 = Delegate1(...) var id_3: Delegate1.Value { /* synthetized get */ // Added a custom set. set { print(newValue) } } var $id_4 = Delegate2(...) var id_4: Delegate2.Value { // Opt out from synthetization for both get and set get { fatalError() } set { print("log: swift is great") $id_4.value = newValue } } var $id_4 = Delegate2(...) var id_4: Delegate2.Value { get { // It's not required to link it to the delegate! return Delegate2.Value() } /* synthetized set */ }
-
-
A property delegate is not restricted to the
$
prefixed identifier space, but as mentioned above the opposite does not apply ($
identifier space is restricted to property delegates). If the user decides to not use such an identifier he/she will lose the automatic synthetization of the computed property that delegates to the property delegate.var _id_5 = Delegate1(...) var id_5: Delegate1.Value // error: stored property not set/implemented var _id_6 = Delegate1(...) var id_6: Delegate1.Value { return _id_6.value // okay - manually routed to the delegate. }
-
This set of rules already hides the storage from the property user even if the property delegate itself has a greater access level.
-
The user can expose the delegate if he needs to.
public struct S { var $property = Delegate1(...) // `Delegate1.Value` == `X` public var property: X } /* Exported as: * public struct S { * public var property: X { get } * } */ public struct T { public var $property = Delegate1(...) public var property: Delegate1.Value } /* Exported as: * public struct T { * public var $property: Delegate1 { get set } * public var property: X { get } * } */
This should be the main functionality of property delegates as it allows creation of very complex delegate types with very little trade-off in typing/initializing/binding the delegate to the computed property.
Going forward we can provide a new syntax form that will trigger automatic synthetization of the property delegate itself. This however requires the compiler to know how it can initialize the property delegate, which is the most problematic challenge that needs to be solved properly.
Here I would like to ask the reader how he/she thinks should the compiler know how the property delegate can be initialized?!
That is not trivial as the original proposal makes it seem. Requiring a fixed set of possible ways of initialization makes property delegates highly restricted and inflexible.
Let us consider an extra attribute first. Such an attribute may only be applied one single init
that has no arguments, provides default values for all arguments, or has maximal one argument that has no default.
@propertyDelegate
struct DelegateX {
@initForSynthetization
init() { ... } // okay
...
}
@propertyDelegate
struct DelegateY {
@initForSynthetization
init(arg1: Arg1 = ..., arg2: Arg2 = ...) { ... } // okay
...
}
@propertyDelegate
struct DelegateZ {
@initForSynthetization
init(arg1: Arg1 = ..., arg2: Arg2) { ... } // okay
...
}
var something: Value by DelegateZ = Arg2()
@propertyDelegate
struct DelegateA {
// error: more then one arguments have no default value
@initForSynthetization
init(arg1: Arg1 = ..., arg2: Arg2, arg3: Arg3) { ... }
...
}
That again would be a wall in the further evolution of that feature.
Now instead of requiring a default value for the delegate's value
, let's say that on the RHS of the assignment we always would require the user to provide the property delegate itself. That however should not mean that we should abandon the possibility of manual implementation for property delegate storage in the $
identifier namespace, as it still would be very handy. What that really means is that the @initForSynthetization
attribute would be unnecessary and we can create a nice syntax that allows us to omit most of the manual typing work and the compiler would always know how to initialize the storage.
@propertyDelegate
public struct MyDelegate {
init(closure: () -> Bool, arg: Int) { ... }
public typealias Value = X
public var value: Value { get { ... } set { ... } }
}
public class HostWithManualImplementation {
public var isSwiftAwesome = true
/* implicitly internal */ private(set) lazy var $property_1 = MyDelegate(
closure: { [unowned self] in self.isSwiftAwesome },
arg: 1
) // MyDelegate.Value == X
public var property_1: X // synthetized get & set
public lazy var $property_2 = MyDelegate(
closure: { [unowned self] in self.isSwiftAwesome },
arg: 2
) // MyDelegate.Value == X
public var property_2: X // synthetized get & set
}
public class HostWithSynthetization {
public var isSwiftAwesome = true
public var property_1: X by /* implicitly internal */ private(set) lazy MyDelegate = MyDelegate(
// ^~~~~~~~~~~~~~~~~~~~~~~~~ ^~~~~~~~~~~~ ^~~~
closure: { [unowned self] in self.isSwiftAwesome },
arg: 1
)
public var property_2: X by public lazy MyDelegate = MyDelegate(
closure: { [unowned self] in self.isSwiftAwesome },
arg: 2
)
// Now we gain:
// - Same access control.
// - We should be able to mark the delegate even with `private(set)`
// and allow overall access to it, but only allow it to be set
// by the container type only.
// - `$property_1` can be accessed internally.
// - We can make the property delegate `lazy` and it can access `self`
}
/* Both type export as:
*
* public class HostWithManualImplementation {
* public var isSwiftAwesome: Bool { get set }
* public var property_1: X { get set }
*
* public var $property_2: MyDelegate { get set }
* public var property_2: X { get set }
* }
*
* public class HostWithSynthetization {
* public var isSwiftAwesome: Bool { get set }
* public var property_1: X { get set }
*
* public var $property_2: MyDelegate { get set }
* public var property_2: X { get set }
* }
*/