Optional Protocol Requirements Members
This post tries to summarise the challenges, the solutions to the challenges, and the problems of the solutions to the challenges brought here ~8 years ago. Many things have changed in Swift since then and I believe it's worth having a fresh look at the issues.
Consider Swift framework. No Objective-C, no NSObject, and, sadly, no optional protocol members. (I'm deliberately using "protocol members" name firstly to account for not just functions but variables, and secondly to avoid the "optional requirements" oxymoron).
API protocols do evolve. Take URLSessionDataTaskDelegate protocol for example – it was changed at least 5 times in different OS versions. It's great to have optional methods: old code that doesn't know about them would still compile, the old binary would still run. Often times protocol default implementation (done in a protocol extension) is brought up as the way to go. There are a few gotchas with that approach in practice.
- Default implementation is not enough sometimes.
Sometimes API needs to know if the client implemented method or not to act differently. Example:
CBCentralManagerDelegate.centralManager(CBCentralManager, willRestoreState: [String : Any])).
Depending upon whether this method is implemented or not the API acts differently. Compared to Objective-C in Swift there is no way to say if a type provided protocol method implementation, other than resorting to hacks:
protocol P {
func foo()
}
extension P {
func foo() {
someTaskLocalVariable += 1
}
}
let old = someTaskLocalVariable
delegate.foo()
let methodImplemented = someTaskLocalVariable == old
- Default implementation could be expensive:
protocol P {
func foo() -> ExpensiveToCreateType
}
extension P {
func foo() -> ExpensiveToCreateType {
.init() // expensive
}
}
If the client doesn't implement this method and there was a way to query if the method is implemented by the client or not – the system could skip calling it's default implementation and do things more efficiently.
- Splitting a protocol into several (as a means to support backward compatibility):
Would result in a serious protocols proliferation.
public protocol URLSessionTaskDelegate : URLSessionDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void)
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest?
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping @Sendable (InputStream?) -> Void)
func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask) async -> InputStream?
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
}
protocol URLSessionTaskDelegate_macOS_10_12 {
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
}
protocol URLSessionTaskDelegate_macOS_10_13 {
func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping @Sendable (URLSession.DelayedRequestDisposition, URLRequest?) -> Void)
func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest) async -> (URLSession.DelayedRequestDisposition, URLRequest?)
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)
}
protocol URLSessionTaskDelegate_macOS_13_0 {
func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask)
}
protocol URLSessionTaskDelegate_macOS_14_0 {
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStreamFrom offset: Int64, completionHandler: @escaping @Sendable (InputStream?) -> Void)
func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask, from offset: Int64) async -> InputStream?
func urlSession(_ session: URLSession, task: URLSessionTask, didReceiveInformationalResponse response: HTTPURLResponse)
}
// usage from within the API:
if let delegate = delegate as? URLSessionTaskDelegate_macOS_13_0 {
delegate.urlSession(session, didCreateTask: task)
}
- Splitting a protocol into multiple protocols (each optional member getting it's own protocol):
Would result into even more severe protocol proliferation and would be beyond ridiculous:
public protocol URLSessionTaskDelegate_didCreateTask {
func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask)
}
public protocol URLSessionTaskDelegate_willBeginDelayedRequest {
func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping @Sendable (URLSession.DelayedRequestDisposition, URLRequest?) -> Void)
}
public protocol URLSessionTaskDelegate_willBeginDelayedRequest_async {
func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest) async -> (URLSession.DelayedRequestDisposition, URLRequest?)
}
public protocol URLSessionTaskDelegate_taskIsWaitingForConnectivity {
func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)
}
public protocol URLSessionTaskDelegate_willPerformHTTPRedirection {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping @Sendable (URLRequest?) -> Void)
}
public protocol URLSessionTaskDelegate_willPerformHTTPRedirection {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest?
}
public protocol URLSessionTaskDelegate_didReceiveChallenge {
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
}
public protocol URLSessionTaskDelegate_didReceiveChallenge_async {
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
}
public protocol URLSessionTaskDelegate_needNewBodyStream {
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping @Sendable (InputStream?) -> Void)
}
public protocol URLSessionTaskDelegate_needNewBodyStreamForTask {
func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask) async -> InputStream?
}
public protocol URLSessionTaskDelegate_needNewBodyStreamFromOffset {
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStreamFrom offset: Int64, completionHandler: @escaping @Sendable (InputStream?) -> Void)
}
public protocol URLSessionTaskDelegate_needNewBodyStreamForTask {
func urlSession(_ session: URLSession, needNewBodyStreamForTask task: URLSessionTask, from offset: Int64) async -> InputStream?
}
public protocol URLSessionTaskDelegate_didSendBodyData {
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
}
public protocol URLSessionTaskDelegate_didReceiveInformationalResponse {
func urlSession(_ session: URLSession, task: URLSessionTask, didReceiveInformationalResponse response: HTTPURLResponse)
}
public protocol URLSessionTaskDelegate_didFinishCollecting_metrics {
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
}
public protocol URLSessionTaskDelegate_didCompleteWithError {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
}
// usage from within the API:
if let delegate = delegate as? URLSessionTaskDelegate_taskIsWaitingForConnectivity {
delegate.urlSession(session, taskIsWaitingForConnectivity: task)
}
-
Lost argument labels.
When you switch fromfunc foo(x: Int, y: Int)
tovar foo: ((x: Int, y: Int) -> Void)?
you are missing argument labels:var foo: ((Int, Int) -> Void)?
. The best you could do at the declaration site is to keep the internal names:var foo: ((_ x: Int, _ y: Int) -> Void)?
but those names would be invisible at the call site:delegate.foo(1, 2)
-
Variable names clash.
In many cases you'd have to rename the variable to avoid the clash:var tableView: (_ tableView: UITableView, _ titleForHeaderInSection: Int) -> String?
var tableView: (_ tableView: UITableView, _ titleForFooterInSection: Int) -> String?var tableView_titleForHeaderInSection: (_ tableView: UITableView, _ section: Int) -> String?
var tableView_titleForFooterInSection: (_ tableView: UITableView, _ section: Int) -> String? -
Does not compile.
Consider the following example. Not only it introduces an extra nesting level and quite ugly, it doesn't even compile.
protocol P {
var foo: ((_ int: Int, _ string: String) -> Void)? { mutating get }
}
extension P {
var foo: ((_ int: Int, _ string: String) -> Void)? { nil }
}
struct S: P {
var x = 1
var foo: ((_ int: Int, _ string: String) -> Void)? {
mutating get {
{ int, string in
self.x = 1 // 🛑 Escaping closure captures mutating 'self' parameter
}
}
}
}
Thoughts?
What is your view on this? Does Swift need optional protocol members going forward or could it do without?
Previous threads (8 years old).
If you know other threads on the matter please shout, and I'll update this list:
- https://forums.swift.org/t/is-there-an-underlying-reason-why-optional-protocol-requirements-need-objc
- https://forums.swift.org/t/idea-how-to-eliminate-optional-protocol-requirements
- https://forums.swift.org/t/proposal-draft-remove-objc-requirement-for-optional-protocol-methods-properties
- https://forums.swift.org/t/review-se-0070-make-optional-requirements-objective-c-only
- https://forums.swift.org/t/proposal-make-optional-protocol-methods-first-class-citizens