[Discussion] Initializer Lookup in Generic Contexts

SE-0299 introduces a way to provide static members for lookup in generic contexts, and it’s interesting to see if similar approach can be extended to initializers.

See the following example.

protocol ItemProtocol {}

struct Item: ItemProtocol, ExpressibleByStringLiteral {
    let name: String
}

extension ItemProtocol where Self == Item {
    init(stringLiteral: String) {
        self.init(name: stringLiteral)
    }
}

func useItem(_ item: some ItemProtocol) {}

The code above compiles successfully, but it cannot be used as the user may expect.

useItem(.init(stringLiteral: "Item 1"))
useItem("Item 2")
<source>:15:1: error: type 'ItemProtocol' cannot conform to 'ItemProtocol'
useItem(.init(stringLiteral: "Item 1"))
^
<source>:15:1: note: only concrete types such as structs, enums and classes can conform to protocols
useItem(.init(stringLiteral: "Item 1"))
^
<source>:13:6: note: required by global function 'useItem' where 'some ItemProtocol' = 'ItemProtocol'
func useItem(_ item: some ItemProtocol) {}
     ^
<source>:15:10: error: type 'ItemProtocol' cannot be instantiated
useItem(.init(stringLiteral: "Item 1"))
         ^
<source>:15:10: error: referencing initializer 'init(stringLiteral:)' on 'ItemProtocol' requires the types 'Self' and 'Item' be equivalent
useItem(.init(stringLiteral: "Item 1"))
         ^
<source>:16:1: error: global function 'useItem' requires that 'String' conform to 'ItemProtocol'
useItem("Item 2")
^
<source>:13:6: note: where 'some ItemProtocol' = 'String'
func useItem(_ item: some ItemProtocol) {}
     ^

Some of the restrictions can be lifted similarly to SE-0299, but there’re two things different for initializers:

  1. Initializer implementations often touch more private details, but when implementing through an extension we can only build them on top of existing initializers (aka. convenience initializer) instead of building all properties manually. It also means that we cannot build the only initializer in this way.
  2. ExpressibleBy*Literal protocols allow initializing an object with literals implicitly. This won’t work in generic contexts because currently we don’t allow specifying protocol inheritance on extensions now.

For all these use cases, there’re two existing ways to work around:

  1. Use static functions like static func item(_: Item) -> Item, which is available since Swift 5.5. In this way we’ll end up with the .item() boilerplate.
  2. Add an overload for a designated concrete type everywhere the protocol is required. Doing this may be easier thanks to the macro system, but it will make the API surface remarkably larger and is really error-prone.

I’d like to see if there’s some way to settle the problem with a cleverer solution.

2 Likes