Hello Swift community,
I have been working on a "mini-manifesto" whose aim is to set out basic design principles around protocol witness matching and get some agreement from the Swift community on it. One of the reasons why is because I am currently pitching a new form of witness matching and while we've had some discussion on this forum on different kinds of matching (like subtyping, etc), it would be great to have a small document which people can refer to during discussions and use it to create new proposals to lift existing limitations.
This document is currently a draft so I would appreciate your thoughts and feedback on it. Thank you!
Updated version here: https://github.com/apple/swift/pull/29617
Protocol Witness Matching Manifesto (Draft)
Author: Suyash Srijan
(Note: This is a work-in-progress design document)
Introduction
In Swift, protocol witnesses have to match exactly with a protocol requirement. For example:
protocol NetworkClient {
func fetchRequest(request: URLRequest, numRetries: Int, completion: @escaping (Result<Data, Error>) -> Void)
}
class MyNetworkClient: NetworkClient {
func fetchRequest(request: URLRequest, numRetries: Int, completion: @escaping (Result<Data, Error>) -> Void) { ... } // Okay
func fetchRequest(request: URLRequest, numRetries: Int, completion: (Result<Data, Error>) -> Void) { ... } // Also okay
func fetchRequest(request: URLRequest, numRetries, someArgument: Int = 0, completion: @escaping (Result<Data, Error>) -> Void) { ... } // Not a match
}
protocol ViewContainer {
var base: UIView { get }
}
class MyViewContainer: ViewContainer {
let base: UIView // Okay
let base: MyCustomUIViewSubclass // Not a match
}
protocol FileDecoder {
func decode() throws -> Data
func encode(using encoder: FileEncoder)
}
class MyFileDecoder: FileDecoder {
func decode() throws -> Data { ... } // Okay
func decode() -> Data { ... } // Also okay
func encode(using encoder: FileEncoder) { ... } // Okay
func encode(using encoder: FileEncoder) throws { ... } // Not a match
}
with certain exceptions, such as:
- A non-throwing function can witness a throwing function requirement.
- A non-failable initializer can witness a failable initializer requirement.
- A non-escaping closure can witness one that is marked
@escaping
.
Now, while some of the rules around matching a witness to a requirement are reasonable, others might argue that some of them are too restrictive (such as disallowing subtyping, which is already allowed in classes). This leads us to ask a very basic question - what kinds of differences are reasonable when matching a protocol witness to a protocol requirement?
This manifesto lists some possible changes we can make to the current model and set out some basic design principles. Some of these changes have been discussed many times in the past on the forums and while there is no guarantee that all of these changes will be implemented (either now or in the future), this document will serve as a starting point for discussions around witness matching (such as discussions around potential designs of how a certain feature mentioned here should work, whether its the right thing to do, etc) and something the community can use to create and publish evolution proposals.
Likely
These are restrictions that are currently enforced by the compiler which are not unreasonable to be allowed and doing so is only a matter of doing the implementation work and does not require any changes to the current syntax.
These are also features that have been requested from time to time and can help improve the expressivity of the language, without fundamentally changing it.
Desugaring
We can enable certain forms of "desugaring", such as allowing enum cases to satisfy static requirements:
For example:
protocol Number {
static var zero: Self { get }
static func one(times: Self) -> Self
}
enum MyNumber: Number {
case zero
indirect case one(times: Self)
}
let foo: some Number = MyNumber.one(times: .zero)
Enum cases already behave like static properties/functions in the language, both syntactically and semantically, so it's reasonable to think of a case as "sugar" for it.
Related bugs:
- [SR-3170] Enum cases should satisfy static var {get} protocol requirements · Issue #45758 · apple/swift · GitHub
- [SR-9099] Enum case does not satisfy protocol static var · Issue #51596 · apple/swift · GitHub
Default arguments
We can allow functions with default arguments to witness function requirements:
protocol NetworkClient {
func fetch(request: URLRequest)
}
final class MyNetworkClient: NetworkClient {
func fetch(request: URLRequest, retryCount: Int = 3) {
...
}
}
Related bugs:
- None
Subtyping
We can allow protocol witnesses to be covariant (i.e. have subtypes of the requirement):
protocol Scrollable {
var base: UIView { get }
func getView() -> UIView
}
final class MyScrollView: Scrollable {
let base: MyCustomSubclassOfUIView
func getView() -> AnotherSubclassOfUIView {
...
}
}
We already allow this in classes, so it probably makes sense for protocols witnesses to follow the same rules that exist for classes, for example, optional sub-typing:
class A {
func getNumber() -> Int? { return 0 }
}
class B: A {
override func getNumber() -> Int { return 0 }
}
As we loosen the rules around subtyping and other kinds of matching, it is possible that associated type inference could degrade and it is important to keep that in mind. There have been some ideas around how to improve it though, such as ones mentioned in this post.
Related bugs:
- [SR-522] Protocol funcs cannot have covariant returns · Issue #43139 · apple/swift · GitHub
- [SR-1950] Read-only protocol property requirements not satisfied by subtypes, when they should be · Issue #44559 · apple/swift · GitHub
Old PR with possible implementation: https://github.com/apple/swift/pull/8718
Maybe/Unlikely
These are features where either it's not clear whether it belongs to the language or provides any concrete benefits, or it complicates the existing design or introduces other implementation/runtime complexity.
Properties with function type
We could allow a function to match a property protocol requirement with a function type. For example:
protocol Fetchable {
var fetch: () -> Data
}
struct MyFetchable: Fetchable {
func fetch() -> Data {
...
}
}
or vice versa - letting a function requirement be witnessed by a property with function type:
protocol Fetchable {
func fetch() -> Data
}
struct MyFetchable: Fetchable {
var fetch: () -> Data {
...
}
}
Syntactic matching
We could allow anything that meets the syntactic needs of the requirement to witness that requirement. For example:
protocol Mathable {
static func isEven(_ instance: Self) -> (Int) -> Bool
}
struct Math: Mathable {
func isEven(_ number: Int) -> Bool {
...
}
}
We could go to the extreme and allow other kinds of matching, such as allowing @dynamicMemberLookup
to satisfy arbitrary protocol requirements.