That would require Xcode to recognize that my SynchronizedLazyOutlet
delegate wraps an IBOutlet
delegate. It's possible, but I don't know if it's probable.
True, also I donât know how this should be communicated with Apple, as IBOutlet delegate is not really part of SE but rather of apple platform frameworks. If IBOutlet was an open class then maybe we could create sub-classes to further customize the behavior for our needs.
I like the keyword âusingâ.
Regarding IBOutlet, I still don't see exactly how the delegate version is supposed to work , as AFAIK the actual annotation does nothing but declare the variable as @objc and is used as a marker in the source code for Xcode to tell it which property is an outlet.
I recognize that this is likely not within the scope of property delegates specifically but it is part of the lazy
story, in my opinion.
will there ever be away to create a property delegate that somehow informs the compiler that laziness is at play to the extent that a reference could be used in its own definition?
I really like the general idea, but I think we should find a way to restore access to the self of the enclosing type.
I think the easiest way to do that is to replace the "value" property with two functions that just take that information:
//Feel free to name these better
func value<S>(enclosedBy: S) -> T
func setValue<S>(_ newValue: T, enclosedBy: S)
Then the main property would just be implemented like this (by the compiler):
var $foo = Delegate<T>
var foo:T {
get {return $foo.value(enclosedBy: self)}
set {$foo.setValue(newValue, enclosedBy: self)}
}
The average user would see no change in syntax. Only implementers of new property delegates would have to know about the functions...
This is a change we would need to make now, because it couldn't be easily added once we require the use of the value property.
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 }
* }
*/
The manual control can allow us to opt in and modify the setter so we gain back willSet
and didSet
if required.
var $property: Delegate = Delegate(...)
var property: X {
// get is synthetized
// opt-out on the synthetization of the setter
set {
print("willSet - newValue: \(newValue)")
let oldValue = $property.value
$property.value = newValue
print("didSet - oldValue: \(oldValue)")
}
}
We could consider a simplification here, but I think the above is already good enough.
var property: X by Delegate = Delegate(...) {
// get is synthetized
// opt-out on the synthetization of the setter
set {
print("willSet - newValue: \(newValue)")
let oldValue = $property.value
$property.value = newValue
print("didSet - oldValue: \(oldValue)")
}
}
Real world example where we can make a type that has value: Value { get }
into a property delegate and still allow mutation. Apply @propertyDelegate
to BehaviorRelay
(let the compiler resolve the issue with generic type parameters being not available for qualified lookup - or just match the types through value: Value
directly).
https://github.com/ReactiveX/RxSwift/blob/master/RxCocoa/Traits/BehaviorRelay.swift#L25
private let $property = BeahaviorRelay<Int>(value: 42)
public var property: Int {
// get is synthetized
// manual routing of the setter
set {
$property.accept(newValue)
}
}
// or
public var property: Int by private BeahaviorRelay = BeahaviorRelay<Int>(value: 42) {
// get is synthetized
// manual routing of the setter
set {
$property.accept(newValue)
}
}
@Douglas_Gregor that example also got me thinking: how do we express with the by
syntax that the property delegate itself should be a constant (as it can be a class)?
Hello all,
I've built a toolchain containing a partial implementation of this feature. You can grab the Linux toolchain or macOS toolchain. At present, there are three main issues with it:
- libSyntax/SwiftSyntax support is missing
- Backing storage properties for locally-scoped properties with delegates can't be found by name lookup
- Many restrictions are not yet implemented
However, you should be able to try out this feature and see how it feels!
Doug
@Douglas_Gregor I played around with it a little. I've seen there is some code related to laziness in the implementation, however I could not make the property delegate lazy
, but you already said that it's partly implemented which is fine.
- Will you support
var property Value by lazy Delegate(object: self, value: Value())
? - Is it possible to remove the restriction of a single generic type parameter and let the compiler infer the type from
value
property on the delegate? I think that restriction is unnecessary as we could have more complex generic property delegate type where the type forvalue
is not even generic itself. - Can the delegate's access modifiers behave the same way as they would on a normal property? (e.g.
public var property: Value by internal private(set) Delegate(...)
)
If these things were possible, then most of my concerns are already eliminated.
If then we could also partly or completely opt-out of the synthethization of the the get/set and override the get/set
(or add a set
) we will be able to get back willSet/didSet
that way. See my posts above for more examples.
var property: X by Delegate(...) {
// get is synthetized
// opt-out of the synthetization of the setter
set {
print("willSet - newValue: \(newValue)")
let oldValue = $property.value
$property.value = newValue
print("didSet - oldValue: \(oldValue)")
}
}
I think this is not aligned yet, as you can share property delegates between modules and the user should have access to value
.
@propertyDelegate
public struct Delegate<T> {
let value: T // should require the same access level as the type
}
A different name to consider:
var property: Int
from Storage(value: 42)
My naive instinct would be to somehow pass inout self as an argument to the delegateâs get/set methods ⌠though this:
- doesnât allow access to
self
in the init (maybe a good thing in disguise?) and - seems like it creates a circular modification between the containing type and the delegate.
In other words, I got ⌠well, not nothing, but epsilon.
Sure ⌠and, if I follow you, that means itâs impossible to do something like this:
class Foo<T: PropertyDelegate> {
var x: Int by T
}
âŚbecause PropertyDelegate
canât just be a protocol, but has to include the higher-kinded âtakes one type parameterâ constraint?
That seems like something to address in Future Directions. Needing that kind of composability, while esoteric, will certainly be a hard wall for developers to hit.
Thanks for the answers! Looking forward to seeing this feature materialize. FWIW, I can imagine several uses and none run up against the above constraints.
I'm trying to follow the thread and understand what is going on. Is it right that the idea of nested property delegates is essentially a property being backed by a storage which in turn is backed by another storage, ... and so on and so forth to potentially unlimited level of nesting?
That sounds like an accurate description to me
Very very happy to see this come back around. Note: I read Doug's proposal, but haven't read all the posts in this thread, sorry for anything redundant here.
âBrandingâ question: why not call them property macros? Doing so suggests a better (IMO) syntax: instead of the weird kotlin syntax:
var foo by Lazy : Int = 1738
You get the nicer syntax of:
#Lazy var foo : Int = 1738
This is excused by saying that â# is for macroây stuffâ and that syntactic expansions are (hygienic!) macro-like. I expect to see more macroây stuff done like this in the future (e.g. when we push protocol synthesis out of the compiler some beautiful day), and this gives us a common conceptual place to hang all of it.
$
Syntax and access control: The synthesis and exposure of the $
members is really unfortunate and doesn't feel swifty at all. This is particularly egregious given you have two access control modifiers going on:
public var foo: Int by public Lazy = 1738
This does not dovetail well with the rest of the language and will be a inscrutable to non-experts in the feature.
In terms of access control, I believe that you are sugaring something that doesnât really matter. If users have direct access to the storage already, they can just explicitly redeclare it as another property, and use a propery macro to make that not totally onerous:
#Forward public var fooStorage = $foo
It is super obvious that this declares a public var named fooStorage
!
In terms of access to the underlying storage, there are lots of things we should consider here, including stuff like #storage(yourObject.foo)
, which would be equivalent to yourObject.$foo
in your proposed syntax, but is more explicit about what is going on.
I partly agree that the access control is an issue because the property declaration line becomes quite complicated:
public var property: Value by internal private(set) ValueDelegate(...)
Aside the issue I see with the restriction of a property delegate being a generic type with a single generic type parameter, can you (@Chris_Lattner3) explain why is the $
prefixed identifier space for property delegates is not Swifty from your perspective?
In my posts above I mentioned that I would prefer a little more explicit control over the property delegate storage declaration, but with automatic synthetization of the getter and setter (if present) linked to the property delegate (I'd prefer to call it a property storage to be honest).
Consider this more explicit example where the access control behaves the same on the storage as it does in the language today
internal private(set) lazy var $property = IntDelegate(self)
public var property: Int {
// * synthesized
// * user can opt-out just from the get synthetization
get {
return $property.value
}
// * synthesized if `$property.value` has a setter
// * user can opt-out just from the set synthetization,
// this can allow the user to implement
// `willSet / didSet`
// * (§) user can explicitly provide a `set` even
// when when `$property.value` does not have a
// setter and implement the behavior
set {
print("willSet")
// Iff `$property.value` is available!!!
$property.value = newValue
print("didSet")
}
}
To communicate the full synthetization to the compiler one would either omit the { }
body from the computed property or explicitly provide an empty body (I'm fine with both).
// Let us assume `@IBOutlet` is a 'custom' attribute from `UIKit/AppKit/...`
private lazy var $label = CustomDelayedImmutable<UILabel>(owner: self)
@IBOutlet public var label: UILabel {} // or we omit `{}`
private lazy var $views = CustomDelayedMutable<[UIView]>(owner: self)
@IBOutlet public var views: [UIView] {}
The analogy for (§):
class A {
var value: Int {
return 42
}
}
class B: A {
override var value: Int {
get { return super.value }
// Added a setter!
set { print(newValue) }
}
}
I think this was already proposed, but I can't find it so I apologize to the the author. What about using the syntax users are already familiar with? Something in line with get / set:
public var name: String {
storage: Lazy
}
It's concise, yet familiar syntax. Body would be optional, but one could have it:
public var name: String {
storage: Lazy {
return Lazy("Test")
}
}
Alternatively we could treat it as a variable, not a function:
public var name: String {
storage: Lazy = Lazy("Test")
}
Memberwise initialization would look like this:
public var name = "Test" {
storage: Lazy // compiler provides { return Lazy("Test") }
}
The access would follow what's in the proposal, but changing it would be natural to a Swift user:
public var name: String {
public storage: Lazy {
return Lazy("Test")
}
}
There would be no $ or other special characters and properties polluting the namespace, rather one would access the underlying storage through a special function, something in line with @Chris_Lattner3 suggestions:
storage(of: name) // Lazy<String>
Alternatively it could be a subscript operator on the object taking a key path:
self[storageOf: \.name] // Lazy<String>
Properties that are not storage backed could just return their own type:
storage(of: notBackedString) // String
Proposal specifies that properties with delegates would not be visible through protocols, but this syntax could allow it if ever desired:
protocol Named {
var name: String { storage: Lazy }
}
If the $
prefixed identifiers are really such a problem, how about an explicit link from a computed property to a storage property that has a type marked as @propertyDelegate
which has a property value
with the same type as the computed property? The computed property will still synthetize the getter and setter and the user can still opt-out of the synthetization and override it.
private lazy var _views = CustomDelayedMutable<[UIView]>(owner: self)
@IBOutlet public var views: [UIView] by _views {
/* synthetized (opt-out-able) get/set */
}
or even something using key-paths:
// `((Reference-)Writable-)KeyPath<Self, Delegate>`
// where `type(of: Delegate(...).value) == Set<Value>.self`
public var values: Set<Value> by \.storage.intermediate.setOfValues {
/* synthetized (opt-out-able) get/set */
}
Here is my mental model
Let me re-name @propertyDelegate
to @propertyStorage
.
Introduction of @propertyStorage
-
@propertyStorage
can mark any type (not retroactively). - A type annotated with
@propertyStorage
must implement a property namedvalue
. -
value
property must have the same access level as the type.
There are no more restrictions or requirements for @propertyStorage
.
Computed property grammar will be extended to allow automatic synthetization of its getter and setter with the following syntax.
var property: T by <key-path-placeholder> {}
The key-path after the by
keyword must infer to ((Reference-)Writable-)KeyPath<Self, S>
where S
is a type annotated with @propertyStroage
. The compiler then will verify statically if S.value is T == true
where T
is the type of the computed property.
On success the compiler will synthetize the getter and optionally the setter to the value
from S
. The get
and set
of a computed property with a storage keypath will be extended to provide a storageKeyPath
identifier that is explicitly declared after the by
keyword.
var property: T by <key-path-placeholder> {
// synthetized
get {
return self[keyPath: storageKeyPath.appending(path: \.value)]
}
// synthetized
// optionally if `S.value` has a setter
set {
self[keyPath: storageKeyPath.appending(path: \.value)] = newValue
}
}
The user is allowed to opt-out of the synthetization (partly or completely) to be able to override its behavior manually. For example:
var property: T by <key-path-placeholder> {
// synthetized get
// manually implemented
set {
print("willSet")
self[keyPath: storageKeyPath.appending(path: \.value)] = newValue
print("didSet")
}
}
The user can also provide a custom set
implementation even if the value
property from the storage does not provide a setter.
var property: T by <key-path-placeholder> {
// synthetized get
// manually implemented
set {
// self[keyPath: storageKeyPath.appending(path: \.value)] = newValue // error `value` is immutable
print(newValue)
}
}
by <key-path-placeholder>
is not exposed to public API but is rather an implementation detail.
On failure the compiler will raise a type mismatch error S.value is T == false
Theoretically we don't even need the @propertyStorage
attribute and simply require the keypath to reference the storage directly, which does not request it to be a value
property. It would only require to be of type:
((Reference-)Writable-)KeyPath<Self, T>
if I understand correctly these two are equivalent:
var foo: Int by Lazy = 123
var foo: Int by Lazy(initialValue: 123)
I don't like that there are two ways to do the same thing. You cannot ban the second style, because sometimes you have to use it:
var bar: Int by Clamped(to: 0...300) = 123 // this is not valid
var bar: Int by Clamped(to: 0...300, initialValue: 123)
How about not implementing the first style?
I also don't like the word by
. Just like bjhomer, I have no idea what it means in a sentence.
I proposed to put a delegate {âŚ}
where you would put get/set or property observers but I like your spelling even better. I think putting the feature here is a lot more consistent with the rest of the language and doesn't make the variable declaration unbearably long and convoluted. I am a bit puzzled that no one seems to be picking up on this.