Pitch: Property Delegates

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’.

1 Like

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.

1 Like

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 the by 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 with by 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 the get/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.
  • 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 = ... or var 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 from value, and infer whether the computed property should only synthetize get or both get and set.

      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 a set while the property delegate's value is get 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 }
 * }
 */
1 Like

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)")
  }
}
1 Like

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)?

2 Likes

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

6 Likes

@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 for value 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:

  1. doesn’t allow access to self in the init (maybe a good thing in disguise?) and
  2. 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?

1 Like

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.

5 Likes

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 }
}
10 Likes

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 named value.
  • 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. :face_with_monocle: It would only require to be of type: ((Reference-)Writable-)KeyPath<Self, T>

1 Like

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.

1 Like

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. :thinking:

4 Likes