Pitch: Property Delegates

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