I think this is a great observation, and I wonder whether the addition of variadic generics will allow us to generalize property wrappers to abstract over a variable number of stored properties.
Type wrappers allow libraries to implement property storage and access patterns for all stored properties in a type, which can be opted into by applying a custom attribute to a type. The functionality is very similar to property wrappers, but there is one single wrapper instance that implements a set of stored properties in a type. Despite being so similar conceptually to property wrappers, type wrappers introduce a lot of new complexity into the language, including:
- A new attribute
@typeWrapper
- A new attribute
@typeWrapperIgnored
- A new initializer form
init(storage:)
- A new indirect property accessor in the form of a subscript:
subscript(propertyKeyPath:storageKeyPath:)
- A synthesized
$Storage
struct specific to each type that is annotated with a type wrapper that stores each of the stored properties written in the wrapped type
- A new transformation using
init(storage:)
, the subscript, and the $Storage
type that turns all stored properties into computed properties.
- DI support for hand written member-wise initialization code that re-writes property initialization to initialization of the wrapper through
init(storage:)
- ...and so on
With variadic generics, there is no longer a need for the synthesized $Storage
struct, and type wrappers can simply be parameterized directly on a pack of stored property types, much like how a property wrapper is parameterized directly on its wrapped value type. If we give property wrappers the capability to abstract over a variable number of stored properties, the majority of the bespoke language capabilities for type wrappers go away. We also get property-wise projections without any new concepts to the language if the property wrapper declares a var projectedValue
, and the ability to store the values however you want, effectively allowing you to write your own $Storage
type.
The property wrapper transform only needs to be slightly tweaked to project out a single element from the pack inside of the computed property accessor. Even without a builtin pack element projection feature, we can already express pack element projection on a variadic generic property wrapper using dynamic member lookup.
Among others, property wrappers that implement different storage patterns, such as CopyOnWrite
should be easily generalizable to multiple wrapped values. For example, you might imagine that a variadic generic CopyOnWrite
property wrapper looks like this:
class Box<Stored...> {
var stored: Stored...
}
@dynamicMemberLookup
@propertyWrapper
struct CopyOnWrite<Value...> {
var box: Box<Value...>
init(wrappedValue: Value...) {
box = Box(stored: wrappedValue...)
}
var wrappedValue: Value... {
get { box.stored... }
set {
if (!isKnownUniquelyReferenced(&ref)) {
box = Box(newValue)
} else {
box.stored = newValue
}
}
}
// We might want to support pack element projection through a built-in pack
// projection feature, but we can also support it through dynamic member lookup
// using positional tuple key-paths.
subscript<U>(dynamicMember keyPath: WritableKeyPath<(Value...), U>) -> U {
get { (wrappedValue...)[keyPath: keyPath] }
set { (wrappedValue...)[keyPath: keyPath] = newValue }
}
}
@CopyOnWrite
struct Person {
var name: String
var birthdate: Date
}
// or
func test() {
@CopyOnWrite var buffer: MyBufferType = ...
}
You can imagine that this code gets expanded to
struct Person {
init(name: String, birthdate: Date) {
_storage = CopyOnWrite(wrappedValue: name, birthdate)
}
var _storage: CopyOnWrite<String, Date>
var name: String {
// using dynamic member lookup, but this could instead use a builtin pack element
// projection on _storage.wrappedValue
get { _storage.0 }
set { _storage.0 = newValue }
}
var birthdate: Date {
get { _storage.1 }
set { _storage.1 = newValue }
}
}
The transformation can easily be tweaked for the experimental enclosing-self subscript by passing in the pack element key-path in addition to the wrappedKeyPath
and storageKeyPath
.
To my mind, generalizing property wrappers achieves the goals of this pitch while building on top of the concepts the language already has for implementing property access patterns.