- 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.Type
intoType<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 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<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 //=> 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. We'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.
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.
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
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 metatype = CustomStringConvertible.self
print(type(of: metatype)) //=> 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
Lets review the following situation:
func isIntSubtype<T>(of: T.Type) -> Bool {
return Int.self is T.Type
}
isIntSubtype(of: CustomStringConvertible.self) //=> false
Now 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 .Type
s for is
and for ==
),
but tends to appear unexpectedly and break subtyping with metatypes.
[1] When
T
is a protocolP
,T.Type
is the metatype of the protocol type itself,P.Protocol
.Int.self
is notP.self
.[2] There isn't a way to generically expression
P.Type
yet.[3] The syntax would have to be changed in the compiler to get something that behaves like
.Type
today.
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) //=> true
In 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) //=> true
When 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) //=> false
We 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.
Here's the core of what I'd say. Use as many or as few of these words as you want, but I think this hits the important points.
Motivation
Every type
T
has an instance, accessible throughT.self
, which represents the type itself. Like all instances in Swift, this "type instance" itself has a type, which is referred to as its "metatype". The metatype ofT
is writtenT.Type
. The instance members of the metatype are the same as the static or class members of the type.Metatypes have subtype relationships which reflect the types they represent. For instance, given these types:
Derived.Type
is a subtype of bothBase.Type
andProto.Type
. That means thatDerived.self
can be used anywhere aDerived.Type
is called for.Unfortunately, this simple picture is complicated by protocols.
Proto.self
is actually of typeProto.Protocol
, not typeProto.Type
. This is necessary because the protocol does not, and cannot, conform to itself; it requires conforming types to provide static members, but it doesn't actually provide those members itself.Proto.Type
still exists, but it is the supertype of all types conforming to the protocol.Making this worse, a generic type always uses
T.Type
to refer to the type ofT.self
. So whenProto
is bound to a generic parameterP
,P.Type
is the same asProto.Protocol
.This shifting of types is complicated and confusing; we seek to clean up this area.
We also believe that, in the long term, the dot syntax will prevent us from implementing certain future enhancements that might be valuable:
Equatable
orCustomStringConvertible
.Proposed solution
We abolish
.Type
and.Protocol
in favor of two generic-style syntaxes:ExactType<T>
is the concrete type ofT.self
. AnExactType<T>
can only ever accept that one specific type, not any of its subtypes.Subtype<T>
is the supertype of allExactType
s whose instances are subtypes ofT
. IfT
is a class,Subtype<T>
would accept anExactType
for any of its subclasses. IfT
is a protocol,Subtype<T>
would accept anExactType
for any conforming concrete type.In this new notation, some of our existing standard library functions would have signatures like:
That last example,
type(of:)
, is rather interesting, because it is actually a magic syntax rather than a function. We propose to align this syntax withExactType
andSubtype
by renaming it toSubtype(of:)
. We believe this is clearer about both the type and meaning of the operation.Future Directions
ExactType
and perhaps onSubtype
to add members or conform them to protocols. This could allow us to remove some standard library hacks, like the non-Equatable
-related==
operators for types.ExactType
as a fairly ordinary final class, moving code from the runtime into the standard library.Subtype(ofType: ExactType<T>, named: String)
pseudo-initializer which would allow type-safe access to classes by name.ExactType
andSubtype
.MemoryLayout
members intoExactType
(presumably prefixed), removing the rather artificialMemoryLayout
enum.Subprotocol<T>
syntax for any protocol requiring conformance to protocolT
.ExactType<T>
include aSubtype
typealias which maps toSubtype<T>
.