Skip to content

Instantly share code, notes, and snippets.

@Anton3
Last active December 5, 2023 07:06
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Anton3/25a66751812f14f76cacc5e382339522 to your computer and use it in GitHub Desktop.
Save Anton3/25a66751812f14f76cacc5e382339522 to your computer and use it in GitHub Desktop.

The metatypes explanation

For every type T in Swift, there is an associated metatype T.Type.

Basics: function specialization

Let's try to write a generic function like staticSizeof. We will only consider its declaration; implementation is trivial and unimportant here.

Out first try would be:

func staticSizeof<T>() -> Int
staticSizeof<Float>()  // error :(

Unfortunately, it's an error. We can't explicitly specialize generic functions in Swift. Second try: we pass a parameter to our function and get generic type parameter from it:

func staticSizeof<T>(_: T) -> Int
staticSizeof(1)  //=> should be 8

But what if our type T was a bit more complex and hard to obtain? For example, think of struct Properties that loads a file on initialization:

let complexValue = Properties("the_file.txt")  // we have to load a file
staticSizeof(complexValue)                     // just to specialize a function

Isn't that weird? But we can work around that limitation by passing instance of a dummy generic type:

struct Dummy<T> { }
func staticSizeof<T>(_: Dummy<T>) -> Int
staticSizeof(Dummy<Float>())

This is the main detail to understand: we can explicitly specialize generic types, and we can infer generic type parameter of function from generic type parameter of passed instance. Now, surprise! We've already got Dummy<T> in the language: it's called T.Type and created using T.self:

func staticSizeof<T>(_: T.Type) -> Int
staticSizeof(Float.self)

But there's a lot more to T.Type. Sit tight.

Subtyping

Internally, T.Type stores identifier of a type, which is an Int value. Specifically, T.Type can refer to any subtype of T. With enough luck, we can also cast instances of metatypes to other metatypes. For example, Int: CustomStringConvertible, so we can do this:

let subtype = Int.self
metaInt    //=> Int
let supertype = subtype as CustomStringConvertible.Type
supertype  //=> Int

Here, supertype: CustomStringConvertible.Type can refer to Int, to String or to any other T: CustomStringConvertible. We can also use as?, as! and is to check subtyping relationships. I'll only show examples with is:

Int.self is CustomStringConvertible.Type  //=> true
protocol Base { }
protocol Derived: Base { }
Derived.self is Base.Type  //=> true
protocol Base { }
struct Derived: Base { }
let someBase = Derived.self as Base.Type
// ...
someBase is Derived.Type  //=> true

A common practise is to store metatypes as Any.Type. When needed, we can check all required conformances.

Dynamic dispatch of static methods

If we have an instance of T.Type, we can call static methods of T on it:

struct MyStruct {
    static func staticMethod() -> String { return "Hello metatypes!" }
}
let meta = MyStruct.self
meta.staticMethod()  //=> Hello metatypes!

What is especially useful, if our T.self actually stores some U:T, then static method of U will be called:

protocol HasStatic { static func staticMethod() -> String }
struct A: HasStatic { static func staticMethod() -> String { return "A" } }
struct B: HasStatic { static func staticMethod() -> String { return "B" } }

var meta: HasStatic.Type
meta = A.self
meta.staticMethod()  //=> A
meta = B.self
meta.staticMethod()  //=> B

Summing that up, metatypes have far deeper semantics than a tool for specialization of generic functions. They combine dynamic information about a type with static information "contained type is a subtype of this". They can also dynamically dispatch static methods the same way as normal methods are dynamically dispatched.

.Protocol

For protocols P, besides normal P.Type, there is also a "restricting metatype" P.Protocol that is the same as P.Type, except that it can only reflect P itself and not any of its subtypes:

Int.self is CustomStringConvertible.Type      //=> true
Int.self is CustomStringConvertible.Protocol  //=> false

I personally don't understand, why P.Protocol is needed. Even without P.Protocol, we can test for equality:

Int.self is CustomStringConvertible.Type  //=> true
Int.self == CustomStringConvertible.self  //=> false

For protocols P, P.self returns a P.Protocol, not P.Type:

let meta = CustomStringConvertible.self
print(dynamicType(meta))  //=> CustomStringConvertible.Protocol

In practise, the existence of P.Protocol creates problems. If T is a generic parameter, then T.Type turns into P.Protocol if a protocol P is passed:

func printMetatype<T>(_ meta: T.Type) {
    print(dynamicType(meta))
    let copy = T.self
    print(dynamicType(copy))
}

printMetatype(CustomStringConvertible.self)  //=> CustomStringConvertible.Protocol x2

Finally, we can understand the confusing situation!

func doesIntConformTo<T>(_: T.Type) -> Bool {
    return Int.self is T.Type
}

doesIntConformTo(CustomStringConvertible.self)  //=> false (?!)

Now we understand that because T is a protocol, T.Type turns into a .Protocol, and we get the confusing behaviour.

The suggestion is to remove P.Protocol from the language.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment