Pitch: Allow functions with default arguments to fulfill protocols

I'm currently trying to write a logger, and generate a protocol for that logger so I can mock it. This is very simplified version of the logger:

class Logger: LoggerProtocol {
    func log(_ message: String, file: String = #file, line: Int = #line) { 
        print "[\(file): \(line)] \(message)"
    }
}

This to me is syntactic sugar for creating two functions:

func log(String)
func log(String, file: String, line: Int)

Unfortunately, Swift does not agree:

protocol LoggerProtocol {
    func log(String) **Swift does not consider Logger to implement this method**
    func log(String, file: String, line: Int)
}

My pitch is to allow the "default" in a protocol. I.E.:

protocol LoggerProtocol {
    func log(String, file: String = default, line: Int = default)
}

This would allow the protocol to more accurately reflect the capabilities of objects that implement it.

As an alternative, making Logger count as implementing LoggerProtocol because it fulfills the call site requirements would also be a reasonable solution:

protocol LoggerProtocol {
    func log(String) **Make Swift consider Logger to be implementing this method**
    func log(String, file: String, line: Int)
}
17 Likes

The subtyping relationships relating to defaulted arguments might be similar to my pitch.

An investigation into implemented either one of these pitches would also likely be able to access the ease of implementing the other. It's worth keeping that in mind.

Slightly off-topic, but why can't log(_:String) be implemented in a protocol extension instead of being a requirement?

This brings back some memories...

Can someone confirm whether or not function extensions with default parameters are true default implementations?

I yes I'd circle back to this thread.

protocol LoggerProtocol {
  default func log(String, file: String = #file, line: Int = #line) {
    ...
  }
}

I think the intention is to be able to do the following:

let logger: LoggerStrategyProtocol = LoggerStrategyA()

From my understanding of the current implementation of protocol extensions, logger would be calling protocol extension instead of a concrete class.

Yes, but this default implementation would simply supply the argument values to log(_:file:line:), which is dispatched dynamically.

Could you please clarify?
This is what I understood:

protocol LoggerProtocol {
    func log(message: String, file: String, line: Int)
}
extension LoggerProtocol {
    func log(message: String) {
        log(message: message, file: #file, line: #line)
    }
}
class Logger: LoggerProtocol {
    func log(message: String, file: String = #file, line: Int = #line) {
        print("[\(file): \(line)] \(message)")
    }
}

Which would give two different results.
Line 6 for:

let logger: LoggerProtocol = Logger()
logger.log(message: "message")

Line 15 for:

let logger = Logger()
logger.log(message: "message")

Since you already have log(message:) as default implementation, there is no need in default values for log(message:file:line:) in the concrete class. In this case, I think, everything will dispatch through the same implementation.

I guess the main problem is that then #file and #line would not be replaced by the correct (ie expected) values.

The file and line information has to be provided by the call site. So actually log(_ message: String, file: String = #file, line: Int = #line) does not fulfill the interface of log(String), as somebody not knowing about those default parameters could not call the method, right?

@laszlokorte is correct. In this specific case, I need it to come from the callee, so protocol extensions don't really work.

More to the point though, to me a protocol requiring a method means that a certain method call is valid against anything that implements the protocol.

protocol A {
   func callMe()
}

class B: A {
  func callMe(a: String = "")
}

B().callMe() <-- This is a valid call, so I would expect it would fulfill the protocol
2 Likes

Sorry, I think I was a bit too quick with my replies. Here is what I actually meant, and could not phrase:

protocol Log {
    func log(_ msg: String, _ file: String, _ line: UInt)
}

extension Log {
    func log(_ msg: String, _ file: String = #file, _ line: UInt = #line) {
        self.log(msg, file, line)
    }
}

struct MyLog: Log {
    func log(_ msg: String, _ file: String, _ line: UInt) {
        print("\(file):\(line) – \(msg)")
    }
}


let l: Log = MyLog()
l.log("hello")

let m = MyLog()
m.log("hello")

This outputs:

Playground.playground:19 – hello
Playground.playground:22 – hello

Which is the right line numbers.

1 Like

I agree with that. Especially given:

protocol A {
    func callMe(_ x: Int)
}

struct B: A {
    func callMe<T: BinaryInteger>(_ x: T) { }
}

This code compiles just fine.

I talked to @Douglas_Gregor about it just now and he says it's not too hard to fix, should anyone be willing to give it a try.

Just noticed this title and felt compelled to share that this exact compiler warning surprised (and delighted) me just earlier today. It caught a bug that would have otherwise gone under the radar.

IMO, any default implementations can and should be provided as an extension to the protocol itself. Not on the concrete type. The power for this warning to prevent bugs is very worth it.

Since this reception seems positive, I've written up a proposal here:
XXXX-protocols-vs-default-arguments. All feedback welcome.