- Proposal: SE-0126
- Authors: Adrian Zubarev, Anton Zhilin
- Status: Revision
- Review manager: Chris Lattner
- Revision: 2
- Previous Revisions: 1
This proposal removes P.Protocol metatypes.
Swift-evolution threads:
- [Revision] [Pitch] Rename
T.Type - [Review] SE-0126: Refactor Metatypes, repurpose T[dot]self and Mirror
- [Proposal] Refactor Metatypes, repurpose T[dot]self and Mirror
- [Discussion] Seal
T.TypeintoType<T>
Explanation of metatypes
For every type T in Swift, there is an associated metatype T.Type.
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 8But 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 functionIsn'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<Properties>())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.
Internally, T.Type stores identifier of a type. 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 //=> IntHere, 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. We'll only show examples with is:
Int.self is CustomStringConvertible.Type //=> trueprotocol Base { }
protocol Derived: Base { }
Derived.self is Base.Type //=> trueprotocol Base { }
struct Derived: Base { }
let someBase = Derived.self as Base.Type
// ...
someBase is Derived.Type //=> trueA common practise is to store metatypes as Any.Type. When needed, we can check all required conformances.
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() //=> BSumming 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.
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 //=> falseEven without P.Protocol, we can test for equality:
Int.self is CustomStringConvertible.Type //=> true
Int.self == CustomStringConvertible.self //=> falseFor protocols P, P.self returns a P.Protocol, not P.Type:
let metatype = CustomStringConvertible.self
print(type(of: metatype)) //=> CustomStringConvertible.ProtocolIn 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 x2Lets review the following situation:
func isIntSubtype<T>(of: T.Type) -> Bool {
return Int.self is T.Type
}
isIntSubtype(of: CustomStringConvertible.self) //=> falseNow we understand that because T is a protocol P, T.Type turns into a P.Protocol, and we get the confusing behaviour.
Summing up issues with P.Protocol, it does not bring any additional functionality (we can test .Types for is and for ==),
but tends to appear unexpectedly and break subtyping with metatypes.
[1] When
Tis a protocolP,T.Typeis the metatype of the protocol type itself,P.Protocol.Int.selfis notP.self.[2] There isn't a way to generically expression
P.Typeyet.[3] The syntax would have to be changed in the compiler to get something that behaves like
.Typetoday.
There is a workaround for isIntSubtype example above. If we pass a P.Type.Type, then it turns into P.Type.Protocol, but it is still represented with .Type in generic contexts. If we manage to drop outer .Type, then we get P.Type:
func isIntSubtype<T>(of _: T.Type) -> Bool {
return Int.self is T // not T.Type here anymore
}
isIntSubtype(of: CustomStringConvertible.Type.self) //=> trueIn this call, T = CustomStringConvertible.Type. We can extend this issue and find the second problem by checking against the metatype of Any:
func isIntSubtype<T>(of _: T.Type) -> Bool {
return Int.self is T
}
isIntSubtype(of: Any.Type.self) //=> true
isIntSubtype(of: Any.self) //=> trueWhen using Any, the compiler does not require .Type and returns true for both variations.
The third issue shows itself when we try to check protocol relationship with another protocol. Currently, there is no way (that we know of) to solve this problem:
protocol Parent {}
protocol Child : Parent {}
func isChildSubtype<T>(of _: T.Type) -> Bool {
return Child.self is T
}
isChildSubtype(of: Parent.Type.self) //=> falseWe also believe that this issue is the reason why the current global functions sizeof, strideof and alignof make use of generic <T>(_: T.Type) declaration notation instead of (_: Any.Type).
Remove P.Protocol type without a replacement. P.self will never return a P.Protocol.
This is a source-breaking change that can be automated by a migrator.
All occurrences of T.Protocol will be changed to T.Type.
Leave .Protocol, but use a separate syntax like .protocol for its creation.
To continue the conversation from Twitter:
The problem I see here is, if
P.selfis not of typeP.Protocol, what is its type?P.Typeis the obvious answer, but that name is already used for something else: the type of all subtypes ofP. For instance:I don't think that can be changed. The root of this behavior—the reason why
P.selfdoesn't conform toP.Type—is thatPdoesn't actually implement the static members expected of a type that conforms toP. For instance:Since
P.selfdoesn't have the static members expected of aP.Type, its type cannot beP.Type. So what is its type?