[Pitch] Object Wrappers

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.>)
1 Like

Using @ on a Type doesn’t make much sense, this is because Swift has consistently used @ to indicate an attachment/attribute to a type. This would make much more sense and be more consistent with the rest of the language to use #, the standalone macro indicator which is small localized additions like adding in a type. # may be a b

Breaking consistency with other features would make it more difficult to apply pre-existing knowledge to this part of the language and could increase complexity within the parser as it has to keep track of conflicting features[1][2]. Even if it lessens perceived “beauty” of your code it would make progressive disclosure when including these features more difficult as the type discriminant is inconsistent with other compiler replacements.


  1. “When you see @ make sure that it is possible to attach a macro to the next object then lookup attached macros” ↩︎

  2. “When you see @ where a type should be and is next to a type name should be replaced with corresponding type” ↩︎

Yes, I was a bit skeptical about the discriminator symbol. I thought it would fit @ since it would be "wrapper/attacher of a type". But thinking the compiler wise, (as you said) it would clash with other things. I did think of using #, but I thought it was only used with macros. I'm going to change it to #.

I would love to know these semantical guidelines. Is there a site or doc that tells these?


  1. “When you see @ make sure that it is possible to attach a macro to the next object then lookup attached macros” ↩︎

  2. “When you see @ where a type should be and is next to a type name should be replaced with corresponding type” ↩︎

Swift Macros were introduced for specialized compiler features to be able to be implemented in a way that follows the prior compiler directives like #error #warning, #if, @autoclosure and @convention in format. If it isn’t at all possible in your desired form factor to build an inline macro or attribute with Swift Macros then you should alter your design.

The Core Team at Apple provided some useful and important context in Expand on Swift Macros particularly right here where they describe the two major types of macros. The document that you wanted is Swift.org’s Macros page in its documentation

ObjectWrapper has no way to conform to SDK private / internal protocols and therefore such design can easily cause unexpected behavior, because wrapper casting to private protocols fails.

There are also newtypes topics previously discussed. This is a more common feature for addressing these needs:

2 Likes

I will update the proposal (soon, hopefully) to address the issues and make my proposal better. I was a bit more vague that I tought. And add what differs from newtype. It's still a newtype proposal but with a different syntax and some more improvements (will add in the update).