Type understanding

I have a simple protocol with a default method:

public protocol Renderable {
    func render() -> String
}

public extension Renderable {
    func render() -> String {
        "[Renderable]"
    }
}

Then there is an advanced class A, which is a Renderable with its own implementation of render()

class A : Renderable {
    public func render(separator: String = "") -> String { ... }
}

Somewhere else a class has a computed property that returns this protocol:

open var test: Renderable { return A(); }

When I call test.render() it outputs "[Renderable]", so the Protocol-method gets called.

I would have expected, that A's overridden render()gets called, because I returned an A.

It does as expected, when I change test to be an A however I would like to keep it general, so that I can assign different objects to it, and then call their specialized render() func.

Seems as if the complier downcasts anything in test to plain Renderable. Can anyone point me in the right direction? What am I understanding wrong here?

The missing part of your understanding is that this is not considered to be an implementation of the protocol requirement render(), because this method takes a single argument—its name is render(separator:)—and the protocol requirement takes none.

Therefore, your class relies on the default implementation of render() to fulfill its protocol requirement. You can see that this is the case just by playing with the concrete type:

let r: () -> String = A().render
r() // prints "[Renderable]"

To address this issue, you can declare public func render() -> String on your class and call the other method (you'll need to provide the default argument explicitly).


The following fact may help to refine your understanding of how Swift handles default arguments—

Given the following function f in library A and a user that calls the function in app B:

// library A
public func f(x: Int = 42) { ... }
// app B
f()

A lot of folks mistakenly think that this would compile as though it were written like this:

// library A
public func f(x: Int) { ... }
public func f() { f(x: 42) }
// app B
f()

Not so. In fact, it compiles as though it were written like this:

// library A
public func f(x: Int) { ... }
// app B
f(x: 42)

In other words, the default value is emitted into the caller (here, app B).

You can observe this difference on platforms where libraries can be distributed separately from the apps that call them. In that case, a future version of library A can change the default argument from 42 to 21 without breaking the functionality of an already compiled app B.

10 Likes

Well explained. So I understand, the Protocol/Extension will not kick in, if the signature does not match exactly (!).

Having learned:

  • methods are not identical by their name, but their name & parameter combination
  • default parameters for methods are not considered when comparing signatures
1 Like

It should be noted that there are some cases where non-exact matches are accepted by the compiler. For example:

protocol P {
    func f() async throws
}
struct S: P {
    func f() {}
    // f matches P’s requirement even though it’s not asynchronous nor throwing
}
1 Like

You can also conform to a fallible initializer with an infallible one:

protocol RawRepresentable {
    associatedtype RawValue
    init?(rawValue: RawValue)
    var rawValue: RawValue { get }
}

struct S: RawRepresentable {
    public var rawValue: Int
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
    // init(rawValue:) matches init?(rawValue:) because Self is a subtype of Self?
}

Sadly, this only works with initializers.

The other situation you might call an "inexact match" is when the witness is more generic than the requirement. The specific condition where it works if there's a fixed set of concrete substitutions that can apply to the witness to make it satisfy the signature of the requirement. For example:

protocol P {
  func f1(_: Int)
  func f2<T: Equatable>(_: T)
}

struct S: P {
  func f1<T>(_: T) {} // T is always Int
  func f2<T>(_: T) {} // T happens to be Equatable but it's not needed
}

The reason that we don't allow arbitrary function subtype conversions is that associated type inference relies on exact matching. Subtype conversions would introduce ambiguity that requires new forms of reasoning to sort out:

protocol P {
  associatedtype A
  func f() -> A
  func g() -> A
}

struct S: P {
  // this is the most specific type that works for both:
  // typealias A = NSObject?
  func f() -> NSString? {}
  func g() -> NSObject {}
}
1 Like

Could the language require explicitly specifying associated types in such situations?

Probably, but existing code might also become ambiguous if previously uninvolved overloads now nearly match, and some are in protocol extensions, and suddenly it's unclear that the previous choice was the "best" one.