Search code examples
swiftgenericstypesprotocolstype-systems

Understanding “Finding the Dynamic Type in a Generic Context”


I would like to understand this section.

I'm reading Finding the Dynamic Type in a Generic Context that has this snippet:

func printGenericInfo<T>(_ value: T) {
    let t = type(of: value)
    print("'\(value)' of type '\(t)'")
}

protocol P {}
extension String: P {}

let stringAsP: P = "Hello!"
printGenericInfo(stringAsP)
// 'Hello!' of type 'P'

... that's followed up by this sentence:

This unexpected result occurs because the call to type(of: value) inside printGenericInfo(_:) must return a metatype that is an instance of T.Type , but String.self (the expected dynamic type) is not an instance of P.Type (the concrete metatype of value).

  1. Why is String.self not an instance of P.Type when I can run this code?
func f(_ t: P.Type) { print("...") }

f(String.self)
  1. Why does type(of:) return the concrete metatype outside but not inside generic functions?
print("'\(stringAsP)' of type '\(type(of: stringAsP))'")
// 'Hello!' of type 'String'

Solution

  • For a protocol P, there are two kinds of metatypes that you can write with P. You can write (any P).Type (aka P.Protocol), which is the metatype of P itself, and any P.Type (aka P.Type), which is an existential type representing "the metatype of some type that conforms to P".

    String.self is an instance of any P.Type, but not (any P).Type. The documentation is incorrect here. It probably meant to say (any P).Type. Otherwise this whole thing doesn't make sense.

    type(of:) has two different behaviours depending on what kind of type its type parameter T is. Your printGenericInfo also has a type parameter T, so to avoid confusion, I will call them typeof.T and printGenericInfo.T respectively.

    If typeof.T is a non-existential type, then it returns the metatype of typeof.T. If typeof.T is an existential (i.e. protocol) type any E, it would return an any E.Type, not (any E).Type. After all, the former is much more useful - it can tell you the actual type that the existential type "wraps". type(of:) in this case needs to "unwrap" the existential and "look inside" of it.

    let s1 = ""
    let s2: Any = s1
    // typeof.T is String
    type(of: s1)
    
    // typeof.T is Any, an existential type, so type(of:) unwraps it and returns String.self
    // this would be pretty useless if it just returned Any.self
    type(of: s2) 
    

    This difference in behaviour is exactly what your printGenericInfo lacks. In fact, this kind of semantics cannot be written in Swift's syntax. This is why type(of:) has the weird signature it has - it returns a Metatype type parameter, seemingly unrelated to the type it takes in.

    type(of:) uses special annotations to allow the compiler to type-check type(of:) calls in a special way. Depending on typeof.T, Metatype could either be typeof.T.Type (when typeof.T is not existential) or any typeof.T.Type (when typeof.T is existential). Note that this is a compile time check. The type parameters of generic functions are decided at compile time, not at runtime.

    printGenericInfo doesn't unwrap printGenericInfo.T and look inside of it. It just directly passes that to type(of:), so typeof.T is decided to be printGenericInfo.T, a type parameter, which is not an existential type.

    When you call printGenericInfo(stringAsP), printGenericInfo.T is decided to be any P - an existential type. This doesn't change typeof.T, which is still printGenericInfo.T, a non-existential type. So at runtime, type(of:) returns the metatype of any P, and that is (any P).Type.

    If you do let t = type(of: value as Any) instead, then type(of:) will see that typeof.T is an existential type (Any), and so it will unwrap the existential type and look inside of it.