Why is a generic function discarding protocol matching information?

Related to an earlier question I asked, I'm trying to validate pairs of types, Models + Presenters. I don't understand why when I implement an associatedtype with a protocol, run time type checking stops working, but only when the types are used inside of a generic function. Sample below:

import Foundation

protocol Diffable {}
protocol PresenterInterface {
    associatedtype Model
    func foo(model: Model)
}
struct MyDiffable: Diffable {}

class PresenterWithProtocolModel: PresenterInterface {
    func foo(model: Diffable) {}
}

class PresenterWithConcreteModel: PresenterInterface {
    func foo(model: MyDiffable) {}
}

func validateMap<M, P: PresenterInterface>(_ m: M.Type, _ p: P.Type) -> Bool {
    return M.self is P.Model.Type
}

print(MyDiffable.self is PresenterWithProtocolModel.Model.Type) // true
print(MyDiffable.self is PresenterWithConcreteModel.Model.Type) // true
print(validateMap(MyDiffable.self, PresenterWithConcreteModel.self)) // true
print(validateMap(MyDiffable.self, PresenterWithProtocolModel.self)) // false

I'm on XCode Version 12.0.1 (12A7300), compiling against whatever version of Swift 5 that shipped with it

I tagged this under compiler development because this seems like unexpected behavior to me :woman_shrugging:

I moved it to Using Swift.

1 Like

As a more general thing: If you see unexpected behavior (and you're not sure if the behavior is correct or a bug), filing a bug report on bugs.swift.org would be one option you could take. It may not be a bug but maybe there's something we can do in terms of diagnostics.

For this specific one, I believe the reasoning has to do with the potentially unexpected behavior of type(of:) in generic contexts, which is explained in the "Finding the Dynamic Type in a Generic Context" section of the documentation.

protocol Diffable {}
protocol PresenterInterface {
    associatedtype Model
    func foo(model: Model)
}
struct MyDiffable: Diffable {}

class PresenterWithProtocolModel: PresenterInterface {
    func foo(model: Diffable) {}
}

class PresenterWithConcreteModel: PresenterInterface {
    func foo(model: MyDiffable) {}
}

func validateMap<M, P: PresenterInterface>(_ m: M.Type, _ p: P.Type) -> (Bool, Bool) {
    return (M.self is P.Model.Type, PresenterWithProtocolModel.Model.Protocol.self == P.Model.Type.self)
}

print(MyDiffable.self is PresenterWithProtocolModel.Model.Type) // true
print(MyDiffable.self is PresenterWithConcreteModel.Model.Type) // true
print(validateMap(MyDiffable.self, PresenterWithConcreteModel.self)) // (true, false)
print(validateMap(MyDiffable.self, PresenterWithProtocolModel.self)) // (false, true)

The check is happening with PresenterWithProtocolModel.Model.Protocol not with PresenterWithProtocolModel.Model.Type.

1 Like

the relevant section for future searchers:

Finding the Dynamic Type in a Generic Context

Normally, you don’t need to be aware of the difference between concrete and existential metatypes, but calling type(of:) can yield unexpected results in a generic context with a type parameter bound to a protocol. In a case like this, where a generic parameter T is bound to a protocol P , the type parameter is not statically known to be a protocol type in the body of the generic function. As a result, type(of:) can only produce the concrete metatype P.Protocol .

This also reinforces my wish for an AnyProtocol type constraint :sob: