Method resolution on protocol extensions

I’ve stumbled upon case with protocol methods in extensions I don’t quite understand how it works.

Suppose we have the following structure:

protocol Base {
}

protocol A: Base {
    func foo()
}

extension A {
    func log() {
        print("This is A")
    }
}

protocol B: Base {
    func bar()
}

extension B {
    func log() {
        print("This is B")
    }
}

class Mixin: A, B {
    func foo() {}
    
    func bar() {}
}

In case Base would’ve have this requirement of log method, the compiler would error due to conflicting declarations. However, in the form as above, next code works fine:

let base: Base = Mixin()
if let a = base as? A {
    a.log()
}
if let b = base as? B {
    b.log()
}

Outputs

This is A
This is B

When base type being down-casted to the specific protocols, I guess it more or less understandable why it works fine — after all it has a clear type for which method should be called.

What I don’t understand how Mixin doesn’t produce an error of having two conflicting methods? It will when you try to call method on its instance

let m = Mixin()
m.log()  // error: ambiguous 

Why conforming to two protocols initially doesn’t result in the error?

1 Like

The two methods are declared in their scopes, the two protocols. They do not conflict.

Mixin declares conformance to both protocols, so its instances can be called with both methods. Here is where the conflict arises: The calling of the proper message, not the declaration of the Mixin type.

It is still possible to call both methods—you just have to tell the compiler which of the ambiguous methods you want. Note that there's no optional downcast involved.

let m = Mixin()
m.log()  // error: ambiguous
(m as A).log() // call A's log()
(m as B).log() // call B's log()

i'm not sure this is a perfect analogy, but if, instead of protocol extension methods, we had something like these global generic functions:

func log(_ instance: some A) { print("A") }
func log(_ instance: some B) { print("B") }

then perhaps the lack of an error until an ambiguous callsite appears makes more sense:

let m: Mixin = Mixin()
let a: any A = m
let b: any B = m

log(a) // ✅
log(b) // ✅
log(m) // 🛑

there isn't actually an issue with having two generic methods with different parameters; it's just a problem if a caller uses an argument that can satisfy either method signature.

4 Likes

That’s actually really helpful to think in that way about these methods, thanks!

So that’s basically down to how Swift resolves types and overloads? Since we can overload by type, methods also kinda overload by type of self roughly speaking?

1 Like

@Slava_Pestov will probably know The Answer, but that sounds plausible to me. if we look at the SIL for the two cases here (protocol extension vs global generic function), you can see they are strikingly similar. the primary differences appear to be the calling convention (makes sense since one is an instance method so has an implicit self parameter), and the signature of the generic parameters (the Self type vs a 'regular' generic type parameter):

// SIL for: A.log()
sil hidden @$s6output1APAAE3logyyF : $@convention(method) <Self where Self : A> (@in_guaranteed Self) -> () {
bb0(%0 : $*Self):
  // implementation appears the same modulo SIL scopes & source locations
} // end sil function '$s6output1APAAE3logyyF'

// SIL for: log(_ `self`: some A)
sil hidden @$s6output3logyyxAA1ARzlF : $@convention(thin) <τ_0_0 where τ_0_0 : A> (@in_guaranteed τ_0_0) -> () {
bb0(%0 : $*τ_0_0):
  // implementation appears the same modulo SIL scopes & source locations
} // end sil function '$s6output3logyyxAA1ARzlF'

3 Likes

Yep, Self is just a generic parameter. When you call foo.bar() where foo is an existential, we follow the same rules as if you did bar(foo) where bar() is a global generic function.

4 Likes