Problem:
In Swift, we currently have no native way to expose a protocol-conforming types without allowing external conformances. This limits our ability to expose existentials like any MyCodeCompatible without risking unwanted conformances (see "uninvited guest" problem recap at the end of the pitch). While there’s a working workaround using @_spi (also discussed at the end of the pitch), Swift may limit SPI in the future, potentially causing this workaround to produce errors.
Solution:
Introducing opaque conformance, where:
- Protocols remain at a stricter access level (f.i.
internal), hidden from users. - An existential type (e.g.,
any MyCodeCompatible) can be exposed with a higher access level using atypealias, likepublic typealias AnyMyCodeCompatible = any MyCodeCompatible. - Users can work with the existential type in collections or other APIs, without being able to conform their own types to the internal protocol.
Key Benefits:
1. API Design Flexibility:
- Framework authors can expose collections or APIs that work with existential types without exposing the protocol itself.
- For example, handling a collection of
AnyMyCodeCompatible:
public var items: [AnyMyCodeCompatible] = []
// Users can interact with this collection, but they can't conform new types to `MyType`.
2. Strict Conformance Control:
- Only predefined types inside the module can conform to the protocol, preventing unwanted external conformances while still allowing use of the existential.
3. Cleaner API
- Expose only the types you want users to work with, while keeping internal implementation details hidden. This prevents accidental misuse or inappropriate conformances and avoids exposing internal types unnecessarily.
Example Use Case:
// Internal protocol, only visible within the framework.
internal protocol MyCodeCompatible {
// Will never be public
static var associatedSecret: MySecret { get }
}
// Internal type needed for protocol conformance.
internal struct MySecret {
let secret: String
init(_ secret: String) {
self.secret = secret
}
}
// Only these types can conform to MyCodeCompatible.
extension Int: MyCodeCompatible {
static var associatedSecret: MySecret { .init("This is an Integer") }
}
extension String: MyCodeCompatible {
static var associatedSecret: MySecret { .init("This is a String") }
}
public struct MyNoSecret: MyCodeCompatible {
static var associatedSecret: MySecret { .init("No secret here") }
}
// Public typealias, allowing users to work with the existential.
public typealias AnyMyCodeCompatible = any MyCodeCompatible
// Public API that uses AnyMyCodeCompatible.
public struct MyFramework {
public var items: [AnyMyCodeCompatible] = []
public init() {
items = [1, "Hello", MyNoSecret()]
}
}
Retrospective on the Problem:
The idea of closing protocol conformance has been discussed in Swift for years to prevent external conformances to public protocols while ensuring stability and control. Contributors like @Karl pitched the sealed protocols proposal, suggesting a "sealed" attribute to address the issue of unwanted conformances, often referred to as the “uninvited guest” problem. Solutions like type erasure were used as workarounds, but they added complexity. Overall, many developers contributed to the discussion, and you can find the detailed recap here.
Workaround:
There is a working workaround to this problem, which is why I suspect it wouldn't be hard to implement. The idea is that visibility of the protocol is restricted by using "@_spi", instead of internal or package, and then existential typealias is surfaced as a public API:
@_spi(Internal)
public protocol MyCodeCompatible {
}
public typealias AnyMyCodeCompatible = any MyCodeCompatible
However, Swift 6+ may introduce errors for SPI exposure.