Please help me understand "Finding the Dynamic Type in a Generic Context"

Please help me 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. How come String.self is not an instance of P.Type when I can run this code?

func f(_ t: P.Type) { print("...") }

f(String.self)

2. 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 Likes

It sounds weird, but very, very strictly speaking you could say "that is because P is not a type at all, but a protocol". I know this is confusing, because "historically" we used protocols in places that usually denote types, but that was just a "shorthand" and by now is better expressed by the keywords some and any.
More concretely breaking down your function f: P.Type does not mean "any instance that adopts P", but rather "Any type object of a type that adopts P". And the type object String.self is the type object of a type that adopts P, String.

Same thing about the "history of Swift". When you write let stringsAsP: P = "Hello!", the more modern, and imo better syntax, would be let stringsAsP: any P = "Hello!". Meaning "the stringAsP variable shall be able to store values of any type that adopts the protocol P". It's a "box", implying a level of indirection like a pointer to the "actual" variable (even though String is a value-semantics type).
When you now use type(of:) outside of your generic function the value gets "unboxed", i.e. a real String typed value is passed to it and you get the output you see.
You can achieve the same thing by, instead of using a generic function, use this:

func printAnyInfo(_ value: any P) {
    print("'\(value)' of type '\(type(of: value)'")
}

(disclaimer: I have not actually tried this... :smiley:)
In this function, you do not unbox the variable, but pass the entire box to the function. type(of:) then works exactly the same as if called outside of it.

Now when you pass the variable to your function, it also gets unboxed, but that happens when it is passed to the function. However, the type of the variable then becomes T, because that is the type by which you must refer to it in the function's implementation. Since you did not constrain T to any protocol, that's pretty much all you get inside of printGenericInfo.

I don't know whether type(of:) could give you more information, but I cannot see a reason for why you would need that. If you need specific steps based on what the actual type is, you use optional casting or is syntax, but better yet, design protocols that encapsulate the needed functionality and constrain the generic function.

1 Like

Sorry, I misquoted on the first question. I meant P.Type. I understand the difference between P, any P, some P, and P.Type. The confusing thing here is "String.self is not an instance of P.Type" but I can define and run this function:

func f(_ t: P.Type) { print("...") }

f(String.self)

This code does mean "String.self is an instance of P.Type".

Did I miss something here?

1 Like

Oh, you're right! I actually misread that quoted article and thought it simply said "not an instance of T.Type" (so simply restating the fact).
Maybe that is indeed a typo in the documentation? Perhaps it should read "not an instance of T.Type but instead an instance of P.Type"?

Obviously the second part of my answer doesn't make much sense now, as I am just explaining what we already know...

2 Likes

Ah, you're right, it should be like this:

... but String.self is not an instance of (any P).Type.

  • (any P).self and P.self are the same thing, it's the value of the type (any P).Type)
  • String.self is the value of any P.Type or P.Type (same type)

You can read more here. (source)

Thank you!

1 Like

Yup, that SO answer explains it way better than what I wrote above. Ultimately the documentation really seems to be wrong, but writing it the way I did is also wrong. I hope in Swift 6 we get rid of the equivalence of P.Type and any P.Type, I think this is contributing to a lot of confusion (though changing it would have some birthing pain for a while...).
It would still be complicated, but at least have one way to write it less...

1 Like