I thought Jordan was going to post it, but instead I'll do it for him in a completely unstructured form (sorry):
class Speaker {
// Once again, the problem with this in today's property wrappers
// implementation is that every instance ends up storing the range, even
// though by construction it can't ever be different from instance to
// instance. That's a real waste. So what if we have a notion of "shared info
// property wrappers" that live on the type and get instantiated?
@Clamped(0...11) var volume: Int = 0
}
@sharedInfoPropertyWrapper
struct Clamped<Value: Comparable> {
var initialValue: Value
var range: ClosedRange<Value>
// First off, the SharedInfo can have its own projectedValue property to
// expose a static $foo property.
var projectedValue: ClosedRange<Value> { range }
// This initializer is the same as for today's property wrappers. However,
// it's initializing the type-level wrapper, not the instance-level one. Like
// current property wrappers, this can take an initial value or not.
//
// This makes a little more sense to me than my previous version purely
// because of how the initializer is invoked at the use site.
init(wrappedValue: Int, _ range: ClosedRange<Value>) {
self.initialValue = range.clamping(wrappedValue)
self.range = range
}
// The `instantiate()` function determines the type of the per-instance
// storage, and is used by default to create instances. A client type can
// always instantiate instances manually too in *its* initializer.
func instantiate() -> Instance {
return Instance(storage: sharedInfo.initialValue)
}
// This is what actually shows up as a stored property on the instance.
struct Instance {
var storage: Value
// Use a subscript so that we have storage semantics but also have access
// to the shared info.
subscript(wrappedValueWithSharedInfo sharedInfo: Clamped<Value>) -> Value {
get { storage }
set { storage = sharedInfo.range.clamping(newValue) }
}
}
}
// One thing I'm not sure about is what to call the Clamped<Value> static
// property. The projection is probably `static var $volume: ClosedRange<Int>`,
// but having the shared info storage be `static var _volume: Clamped<Int>`
// seems a little weird. Nothing good comes to mind, however, especially when
// remembering that people can name their properties with non-English names. If
// we don't care about people accessing it directly we can do something like
// `static var volume$sharedInfo: Clamped<Int>` though.
// How does this scale to the (secret) mechanism for accessing the owner of a
// property? The best way is to provide the key paths that are currently in the
// (underscored) static subscript form of property wrappers directly to the
// initializer. Let's see that with "Observed".
protocol Observable: AnyObject {
func notifyObserversAsync<Value>(for keyPath: KeyPath<Self, Value>)
}
@sharedInfoPropertyWrapper
struct Observed<Owner: Observable, Value> {
var initialValue: Value
var storageKeyPath: ReferenceWritableKeyPath<Owner, Instance>
var wrappedValueKeyPath: ReferenceWritableKeyPath<Owner, Value>
// We use a new kind of default argument here instead of magic names.
// This allows regular arguments and property-wrapper-related implicit
// arguments to coexist better than in the current scheme. Other magic
// arguments could include #propertyWrapperProjectionKeyPath and
// #propertyWrapperOwnerType.
init(
wrappedValue: Value = #propertyWrapperInitialValue,
storageKeyPath: ReferenceWritableKeyPath<Owner, Instance> = #propertyWrapper
StorageKeyPath
wrappedValueKeyPath: ReferenceWritableKeyPath<Owner, Value> = #propertyWrapp
erValueKeyPath
) {
self.initialValue = wrappedValue
self.storageKeyPath = storageKeyPath
self.wrappedValueKeyPath = wrappedValueKeyPath
}
func instantiate() -> Instance { return .init(storage: initialValue) }
struct Instance {
var storage: Value
// This replaces the static subscript we have today. (It could live on
// either the Instance or the outer wrapper type, since it doesn't have any
// context of its own. I put it here for consistency with the non-static
// one, but the outer one would get to use 'Self' in the signature, which is
// a little nice.)
//
// One downside of this approach is that it's a little less easy to support
// working with both mutable and immutable properties. With today's static
// subscript, you can overload on the key path kind; in the shared-info
// version, you'd have to store either a KeyPath or a
// ReferenceWritableKeyPath and keep track. Or just store a KeyPath and try
// downcasting (ick). And it still doesn't give you access to a struct
// value.
static subscript(
_enclosingInstance instance: Owner,
sharedInfo: Observed<Owner, Value>
) -> Value {
get { instance[keyPath: sharedInfo.storageKeyPath].storage }
set {
instance[keyPath: sharedInfo.storageKeyPath].storage = newValue
instance.notifyObserversAsync(for: sharedInfo.wrappedValueKeyPath)
}
}
}
}
// We should also have equivalents of both subscripts for the projectedValue.
// P.S. What happens if you use a property wrapper with shared info on a
// non-instance property? Do you get an error, or does it just put the shared
// info alongside the regular property? In the latter case, what happens with
// the projected value?
// To close out, here are two kinds of Lazy we can do: one that's just like the
// example Doug showed at WWDC but more efficient...
@sharedInfoPropertyWrapper
struct Lazy<Value> {
var compute: () -> Value
init(wrappedValue: @autoclosure () -> Value) {
self.compute = wrappedValue
}
func instantiate() -> Instance { return .init() }
struct Instance {
var value: Value? = nil
subscript(wrappedValueWithSharedInfo sharedInfo: Lazy<Value>) -> Value {
mutating get {
if let value = self.value {
return value
}
value = sharedInfo.compute()
return value!
}
set {
value = newValue
}
}
}
}
class Calculator {
@Lazy var regionalTipRates = Dictionary(contentsOf: …)
}
// ...and one that allows referencing 'self', sort of, at the cost of some
// goofy-looking syntax at the use site.
@sharedInfoPropertyWrapper
struct LazyFromSelf<Owner: AnyObject, Value> {
var compute: (Owner) -> Value
var storageKeyPath: ReferenceWritableKeyPath<Owner, Instance>
init(
_ compute: (Owner) -> Value,
storageKeyPath: ReferenceWritableKeyPath<Owner, Instance> = #propertyWrapperStorageKeyPath
) {
self.compute = compute
self.storageKeyPath = storageKeyPath
}
func instantiate() -> Instance { return .init() }
struct Instance {
var value: Value? = nil
static subscript(
instance: Owner,
sharedInfo: LazyFromSelf<Owner, Value>
) -> Value {
get {
if let value = instance[keyPath: sharedInfo.storageKeyPath].value {
return value
}
let result = sharedInfo.compute(instance)
instance[keyPath: sharedInfo.storageKeyPath].value = result
return result
}
set {
instance[keyPath: sharedInfo.storageKeyPath].value = newValue
}
}
}
}
class WindowController {
// Yeah, ick. We'd need some *additional* feature to make this better. I think
// I've gone on long enough, though.
@LazyFromSelf({ (self_: Self) in
self_.loadWindow()
}) var window: Window
}