Pitch: Property Delegates

These last few posts got me thinking. I'm going to propose a different design where the delegate and the storage are two separate things. The delegate defines the storage by having a Storage type (or typealias) inside of it and access the storage to get and set the value using a subscript:

@propertyDelegate 
struct Lazy<Container, Value> {
  enum Storage {
    case uninitialized
    case initialized(Value)
  }

  var initialStorage: Storage
  var initialValue: () -> Value

  init(initialValue: @autoclosure @escaping () -> Value) {
    self.initialStorage = .uninitialized
    self.initialValue = initialValue
  }

  func get(from container: inout Container, valuePath: KeyPath<Container, Value>) {
      switch container[keyPath: valuePath] {
      case .uninitialized:
        container[keyPath: valuePath] = initialValue(value)
        return value
      case .initialized(let value):
        return value
  }
  func set(newValue: Value, in container: inout Container, valuePath: KeyPath<Container, Value>)
      container[keyPath: valuePath] = .initialized(newValue)
  }
  func reset() {
      container[keyPath: valuePath] = .uninitialized
  }
}

Then this code:

$Lazy var property: Int = 5

Would generate something like this:

// the delegate lives as a static variable
static var _property_delegate: Lazy<Self, Int> = Lazy(initialValue: 5)
// the storage is an instance variable
var _property_storage: Lazy<Self, Int>.Storage = Self._property_delegate.initialStorage
// the computed property accesses the value through the delegate
var property: Lazy<Self, Int>.Value {
   mutating get {
      // mutating only when Lazy.get takes `self` as `inout`
      return Self._property_delegate.get(from: self, valuePath: \._property_storage])
   }
   set {
      // mutating only when Lazy.set takes `self` as `inout`
      Self._property_delegate.set(newValue, in: self, valuePath: \._property_storage)
   }
}

Splitting the storage and the delegate allows two things:

  1. access to self in the delegate's getter & setter. In case of Lazy, it means we can pass self to the closure if desired.

  2. storing global per-property metadata and using it in the getter & setter. In case of Lazy, it means we don't have to store the closure for the initial value within every instance.