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:
-
access to
selfin the delegate's getter & setter. In case ofLazy, it means we can passselfto the closure if desired. -
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.