Understanding “Finding the Dynamic Type in a Generic Context”

109 Views Asked by At

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'
2

There are 2 best solutions below

1
Sweeper On BEST ANSWER

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.

3
JanMensch On

The reason is that you instantiate a String by using let = "this is a string".

You do not (and cannot) instantiate a P. P is a protocol and other types can only conform to it. But in the end, when creating a value for a variable, you have to explicitly instantiate a type. And that type might conform to a protocol, or not. Conformance is something else than identity.

Edit, more clarification on your questions:

  1. String.self is not an instance of P. That is because when writing extension String: P {}, you tell it that any instance of String can be seen as a P. That means the type itself String.self, does not conform to P. (Back to the point: conformance is not identity.)

  2. In this line let stringAsP: P = "Hello!" you tell the compiler to explicitly forget that the value is a String. That then causes the compiler to generate this method printGenericInfo with P as a type. You then can think of type(of:) then saying "well, in the method definition you're saying you pass in a P, so I will just repeat that back to you. No need to do hard work to look things up manually.". If you then follow Apple's example of first casting it to Any, type(of:) will actually do some work and look up the explicit type.