Hello Swift community,
I propose a new attribute named @objectWrapper
. This wrapper will wrap to any object (class, struct, enum). Its requirement is having a property called element
of any type.
@objectWrapper
struct Box {
var element: String
}
Motivation
When we import a library, it comes with its own types. But if we need to have it conform to a protocol that we don’t own, most developers will have it retroactively conform to the protocol. But this is dangerous since the library owner can add this conformance later (see SE-0364). Another way to do this is to create a wrapper for the imported type. But if the type needs to conform to a lot of protocols, this can become very time consuming and can generate a lot of boilerplate. And, if the library owner decides to add an extra conformance and we need the conformance, we need to add it to our wrapper type every time. And also, we now need to call the property to every function call site, which is also very inconvenient.
struct MyWrapperType: Identifiable, Hashable ... {
var wrapping: SomeType
var id: String { wrapping.id }
// ...
}
These problems are also true when we create a type (in a library for example) that aims to extend or change the purpose of another type (for example, Binding
). An another problem that occurs is owners of a wrapper inside the library are unable to adress every single conformance. And, the library owner(s) are physically unable to conform to custom protocols created in other modules. For example this library.
struct MakeItIdentifiable<T>: Identifiable {
var wrapping: T
var id: String
}
extension MakeItIdentifiable: Hashable where T: Hashable {
// ...
}
// ...
Detailed design
The element's functions and variables will be directly available to call via the wrapper. The wrapper will also infer the protocol conformances from the element (the protocols that the element conforms to). But it is also possible to add extra conformances. It can also be directly input to functions without calling .element
as long as the element matches the value’s type.
let hello = "mystring"
var box = Box(element: hello) // Now conforms to StringProtocol etc.
func addParenteses(_ string: String) -> String {
return "(" + string + ")"
}
let result = addParenteses(box)
print(result) // "(mystring)"
The wrappers will implicitly conform to the new protocol ObjectWrapper
:
@dynamicMemberLookup
protocol ObjectWrapper<T> {
associatedtype T
var element: T { get set }
subscript<Value>(dynamicMember path: ReferenceWritableKeyPath<T, Value>) -> Value { get set }
subscript<Value>(dynamicMember path: WritableKeyPath<T, Value>) -> Value { get set }
subscript<Value>(dynamicMember path: KeyPath<T, Value>) -> Value { get }
}
extension ObjectWrapper {
subscript<Value>(dynamicMember path: ReferenceWritableKeyPath<T, Value>) -> Value {
get { element[keyPath: path] }
set { element[keyPath: path] = newValue }
}
subscript<Value>(dynamicMember path: WritableKeyPath<T, Value>) -> Value {
get { element[keyPath: path] }
set { element[keyPath: path] = newValue }
}
subscript<Value>(dynamicMember path: KeyPath<T, Value>) -> Value {
element[keyPath: path]
}
}
The type value of these types are going to be annotated with the #
symbol for discriminating against other types. So the type for the value box
is let box: #Box
(some other type would be let wrapper: #SomeWrapper<T>
).
If a function in the wrapper clashes with a function in the element, the wrapper’s function will be favored. To favor the element's functions, explicitly call the .element
.
To explicitly require an object wrapper, its possible to do the below:
func doThis(_ value: some ObjectWrapper<String, T, some Identifiable etc.>)