Skip to content

Instantly share code, notes, and snippets.

@Anton3
Last active July 25, 2016 14:13
Show Gist options
  • Save Anton3/e2b9cda7a32d20786dbe86312d4f7007 to your computer and use it in GitHub Desktop.
Save Anton3/e2b9cda7a32d20786dbe86312d4f7007 to your computer and use it in GitHub Desktop.

Remove .Protocol metatype

Introduction

This proposal removes P.Protocol metatypes.

Swift-evolution threads:

Motivation

Explanation of metatypes

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

Subtyping

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.

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.

Current behavior of .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

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 .Types for is and for ==), but tends to appear unexpectedly and break subtyping with metatypes.

Even more issues with .Protocol

[1] When T is a protocol P, T.Type is the metatype of the protocol type itself, P.Protocol. Int.self is not P.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.

Written by Joe Groff: [1] [2] [3]

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).

Proposed solution

Remove P.Protocol type without a replacement. P.self will never return a P.Protocol.

Impact on existing code

This is a source-breaking change that can be automated by a migrator. All occurrences of T.Protocol will be changed to T.Type.

Alternatives considered

Leave .Protocol, but use a separate syntax like .protocol for its creation.

@beccadax
Copy link

I think this is way too easy for the compiler to catch for us to tolerate it being a static error.

In my opinion, the problem is actually that, on concrete types, Type is conflated with (the equivalent of) Protocol. What I would do is create a (perhaps just for now) magic syntax, Metatype<T>. T.self is always of type Metatype<T>, whether T is a concrete class or a protocol.

protocol P { }
class C: P { }

let c1: Metatype<C> = C.self    // Okay
let p1: Metatype<P> = P.self     // Okay
let p2: Metatype<P> = C.self    // Not okay--C is not the same as P

There is also a T.Type. This is the supertype of all metatypes of subtypes of T. In other words, if U subclasses/conforms to T, Metatype<U> is a subtype of T.Type.

let c1: C.Type = C.self    // Okay
let p1: P.Type = C.self    // Okay
let p2: P.Type = P.self    // Not okay--Metatype<P> is not a subtype of P.Type

Now it is always clear whether you mean "the metatype of T" or "the metatype of any type whose instances are subtypes of T". The former is always spelled Metatype<T>; the latter is always spelled T.Type.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

Are classes subclasses of themselves let c1: C.Type = C.self // Okay?

I believe we should also go for T.Metatype for the consistency.

Do you think the issue for generics would be solved like this:

func canCast<T>(metatype: Any.Metatype, to _: Metatype<T>) -> T.Metatype? {
     return metatype as? T.Metatype
}

// Int.self is fine because it's a subtype of `Any.Type`
// CustomStringConvertible.self is fine because it's not a concrete metatype
// inside the function we grab `T` and check agains the existential `T.Metatype`
canCast(metatype: Int.self, to: CustomStringConvertible.self)

// equivalent to `Int.self as? CustomStringConvertible.Type`

@DevAndArtist
Copy link

Would you like (or do you have some time) to join the party of (re)writing the proposal into that direction?

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

I kinda like this model a lot. If I'm not mistaken here T.Metatype can be replaced later (in Swift 4) by Any<Metatype<T>>?

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

If the example from above would work like I'd expect it, then we found a solution for the currently not implementable type(of:) (dynamicType) function, which also should be called metatype(of:) for consistency.

public func metatype<T>(of instance: T) -> T.Metatype {
      return /* implement */
}

It's also clear that we can cast to a different U.Metatype and might succeed.

@Anton3
Copy link
Author

Anton3 commented Jul 25, 2016

@DevAndArtist

If I'm not mistaken here T.Metatype can be replaced later (in Swift 4) by Any<Metatype<T>>?

Only if subtyping on metatypes remains and only if Any existential container is going to specifically support metatypes, otherwise we end up with sizeof = 40.

@brentdax
This is the direction we took from the beginning. We had static and dynamic metatype types. They could be named Type<T> and Metatype<T>, respectively. But core team insisted that we break the proposal down into atomic parts, that's why we decided to rename Metatype<T> now and add Type<T> later.

Now you insist that we must keep current .Protocol behavior, as well. Why not move its functionality to Type<T>? We can also add size, stride, alignment to Type<T>, because it does not need to dynamically dispatch static functions.

I'm strongly opposed to keeping any magical members. If removal of .self is accepted, we would be the only ones left behind.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

@Anton3

Type<T> does not solve the problem with .Protocol, and just removing .Protocol does not solve everything (I haven't thought about that at first glance):

func cast<T>(metatype: Any.Type, to: Type<T>) -> T.Type? {

    return metatype as? T.Type
}

func `is`<T>(metatype: Any.Type, to: Type<T>) -> Bool {

    print(T.Type.self)
    return metatype is T.Type
}

cast(metatype: Int.self, to: Type<CustomStringConvertible>()) // nil
`is`(metatype: Int.self, to: Type<CustomStringConvertible>()) // false

Int.self is CustomStringConvertible.Type // true
Int.self as? CustomStringConvertible.Type // Int.Type hidden by CustomStringConvertible.Type

The 'gotcha' here is that there are already existential in the language that we don't know of (officially):

T.Type has two roles, as an existential like Brent described and also as a concrete metatype of T.

In Brent's last suggestion he split those apart:

  • where the concrete metatype will become Metatype<T> and is produced by T.self
  • The existential will become more clear and will remain (at least for now), but will be renamed, T.Metatype

@beccadax
Copy link

Are classes subclasses of themselves let c1: C.Type = C.self // Okay?

Yes. Or, to be more precise, Metatype<C> is a subtype of C.Type. If class C has class members, then a Metatype<C> has implementations of those members. Therefore, Metatype<C> can be a subtype of C.Type. (The story is different for a protocol; Metatype<P> does not have implementations of P's static members, so it can't be a subtype of P.Type.)

I find it helpful to think of C.Type as being sort of like a protocol, but one that you can't conform to manually. C.Type includes all of the static members all subtypes of C have to conform to.

I believe we should also go for T.Metatype to be consistency.

I would like them to have distinctive names so that it's more obvious they're not the same thing. Thinking more deeply about the names:

  • Typically, a type name describes the instances, not the type. (For instance, NSURL is so named because its instances represent URLs; it's not called NSURLClass.) Thus, it arguably makes more sense to use Type<T> instead of Metatype<T>.
  • The T.Type syntax is more difficult. We want something that implies it's a type, implies that it allows subtypes, and is conveniently short; I'm not sure how to get all three. A few thoughts:
    • T.Subtype or Subtype<T>: Too jargony?
    • T.CompatibleType or T.CastableType: Too long, perhaps too vague.
    • T.TypeProtocol or TypeProtocol<T>: Too long, too abstract, too hard to remember.
    • T.LikeType: Too cute?

Do you this the issue in generics would be solved like this:

func canCast<T>(metatype: Any.Metatype, to _: Metatype<T>) -> T.Metatype? {
     return metatype as? T.Metatype
}

// Int.self is fine because it's a subtype of `Any.Type`
// CustomStringConvertible.self is fine because it's not a concrete metatype
// inside the function we grab `T` and check agains the existential `T.Metatype`
canCast(metatype: Int.self, to: CustomStringConvertible.self)

// equivalent to `Int.self as? CustomStringConvertible.Type`

Yes, I believe (leaving aside the question of what we call .Type and Metatype) this code is correct.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

The other problem that raises with type names is that we should also signal their compatibility with each other if we'd want to distinguish between concrete and base metatypes.

Let's say we rename concrete metatypes to Type<T>, which means it's a type about a type, or in other words a metatype.

Now we need to find find something for existential T.Type.

  • Base<T> - does look strange when: instance: Base<T> = metatype where metatype: Type<T>
  • BaseType<T> - one could assume this is an actual type
  • ExistentialType<T> - it's quite long, but precise as anything else. Or TypeExistential<T>.
  • Existential<T> - is more like Any<T> which is not a metatype any more
protocol P { }
class C: P { }

let c1: Type<C> = C.self    // Okay
let p1: Type<P> = P.self     // Okay
let p2: Type<P> = C.self    // Not okay--C is not the same as P

let c1: ExistentialType<C> = C.self    // Okay
let p1: ExistentialType<P> = C.self    // Okay
let p2: ExistentialType<P> = P.self    // Not okay--Type<P> is not a subtype of ExistentialType<P>

@Anton3
Copy link
Author

Anton3 commented Jul 25, 2016

@brentdax
I would prefer "dynamic" Subtype<T> plus "static" Type<T>. And we should disallow static method calls on Type<T> for reasons already stated.

@DevAndArtist
Copy link

To summarize:

  • This proposal should be Refactor Metatypes
  • Provide a distinction between concrete and existential metatypes, which should eliminate the .Protocol issue!
    • concrete Type<T> or Metatype<T> or all other bikeshedding names
    • existential T.Type or T.Metatype or ExistentialType<T> or other bikeshedding names (named in alternative section)
  • consider to rename type(of:) to metatype(of:) iff we'd choose something like Metatype<T>
  • Problem with generics solved.
  • Problem with type(of:) solved.
  • T.Type (probably) eliminated.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

One more thing about Subtype<T>: isn't it a Supertype<T> instead?

Type<T> : Supertype<T> - for classes + classes, classes/structs/enums + protocol

What about protocols U : T?

Type<U> : Supertype<T> - does not work like this, right?

@beccadax
Copy link

beccadax commented Jul 25, 2016

You know, let's try reversing this. The concrete metatype is less important than the "protocols" it conforms to. It should be shorter.

protocol P { }
class C: P { }

let c1: ExactType<C> = C.self    // Okay
let p1: ExactType<P> = P.self     // Okay
let p2: ExactType<P> = C.self    // Not okay--C is not the same as P

let c1: Type<C> = C.self    // Okay
let p1: Type<P> = C.self    // Okay
let p2: Type<P> = P.self    // Not okay--ExactType<P> is not a subtype of Type<P>

func canCast<T>(metatype: Type<Any>, to _: ExactType<T>) -> Type<T>? {
     return metatype as? Type<T>
}
func type<T>(of: T) -> Type<T> {  }     // Hmm...could this be an initializer? Type(of: T)
func size<T>(of: ExactType<T>) -> Int {  }

Type could be Subtype; ExactType could be SpecificType. Subtype would be more accurate; SpecificType would be longer (and thus more discouraging).

let c1: SpecificType<C> = C.self    // Okay
let p1: SpecificType<P> = P.self     // Okay
let p2: SpecificType<P> = C.self    // Not okay--C is not the same as P

let c1: Subtype<C> = C.self    // Okay
let p1: Subtype<P> = C.self    // Okay
let p2: Subtype<P> = P.self    // Not okay--SpecificType<P> is not a subtype of Subtype<P>

func canCast<T>(metatype: Subtype<Any>, to _: SpecificType<T>) -> Subtype<T>? {
     return metatype as? Type<T>
}
func type<T>(of: T) -> Subtype<T> {  }
func size<T>(of: SpecificType<T>) -> Int {  }

Those aren't package deals, by the way; we could do Type and SpecificType or Subtype and ExactType.

@Anton3
Copy link
Author

Anton3 commented Jul 25, 2016

@DevAndArtist
An instance of Subtype<T> reflects a subtype of T, looks logical to me.

@brentdax
The most important use case of metatypes in Swift is explicit specialization of generic functions. Static metatypes should be used there "by default", and so I would argue that they are more important.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

An instance of Subtype reflects a subtype of T, looks logical to me.

Yeah it just came to my mind, it's the dynamic behavior so I feel safe here now. ;)

Type(of: T)

Uhh, sexy, but we don't have an actual type Type<T> yet. Deprecating type(of:) for Type<T> should be easy right? <-- Future direction.


I don't like ExactType, it just don't fit there. From all these names I think I'd prefer Type and Subtype.


So the basic idea of this whole model looks visually like this:

protocol P {}
protocol R : P {}

class A : P {}
class B : A, R {}

Subtype<Any> // like Any.Type
Subtype<P> : Subtype<Any>
Subtype<R> : Subtype<P>
Subtype<A> : Subtype<P>
Subtype<B> : Subtype<A>, Subtype<R>

Type<P> : Subtype<P> // but still abstract and cannot call static methods etc.
Type<R> : Subtype<R> // same
Type<A> : Subtype<A> // not abstract anymore, can call static methods etc.
Type<B> : Subtype<B> // same as `Type<A>`

Subtype<T> are basically internal abstract protocols that are applied automatically.

@DevAndArtist
Copy link

We should start rewriting the proposal. I'll be back in a few hours to help. I've got some work to do atm.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

I believe one side effect of this model will open up an easy introduction of abstract classes.

// visually repainted
abstract class X {
    func foo()
}

// is the same as
Subtype<X> : Subtype<Any> {
    func foo()
}

Abstract classes would behave like protocols now where we cannot calls any static methods etc. on them.

@beccadax
Copy link

I've been looking at the standard library since @Anton3's post. As far as I can tell, in general:

  • A .Type return value is usually a Subtype.
  • A .Type non-generic parameter is usually Subtype.
  • A .Type generic parameter is usually ExactType.

I guess my suggestion would be Subtype and ExactType. That seems like the best way to make sure the roles of both types are clear, and it doesn't favor either one over the other. But if you guys want to do Type and Subtype, that's a minor quibble.

@beccadax
Copy link

Type(of: T)

Uhh, sexy, but we don't have an actual type Type yet. Deprecating type(of:) for Type should be easy right? <-- Future direction.

Actually, with Type and Subtype, it would probably be Subtype(of:). Which is arguably a better description of what's going on anyway.

@DevAndArtist
Copy link

Actually, with Type and Subtype, it would probably be Subtype(of:). Which is arguably a better description of what's going on anyway.

Agreed.

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

@brentdax

If only we had you on board from the beginning. :)

@DevAndArtist
Copy link

@brentdax

What would you like us to keep and to drop from the current draft?

@DevAndArtist
Copy link

DevAndArtist commented Jul 25, 2016

Fixed some misbehavior from above:

protocol P { }
protocol R {}
class A : P { }
class B : A, R { }

//===----------------------------------------------------------------------------===//
//===------------------------visual metatype relationship------------------------===//
//===----------------------------------------------------------------------------===//

Subtype<Any> // Like old `Any.Type`
Subtype<P> : Subtype<Any>
Subtype<R> : Subtype<P>
Subtype<A> : Subtype<P>
Subtype<B> : Subtype<A>, Subtype<R>

ExactType<P> : Subtype<Any> // ExactType of protocols are blind
ExactType<R> : Subtype<Any> // ExactType of protocols are blind
ExactType<A> : Subtype<A>
ExactType<B> : Subtype<B>

//===----------------------------------------------------------------------------===//
//===----------------------------------------------------------------------------===//

let c1: ExactType<A> = A.self // Okay
let p1: ExactType<P> = P.self  // Okay
let p2: ExactType<P> = C.self // Error -- C is not the same as P

let a1: Subtype<Any> = A.self // Okay
let c1: Subtype<A> = A.self     // Okay
let p1: Subtype<P> = A.self     // Okay
let a2: Subtype<Any> = P.self // Okay
let p2: Subtype<P> = P.self     // Error -- ExactType<P> is not a subtype of Subtype<P>

func dynamic<T>(type: Subtype<Any>, as _: ExactType<T>) -> Subtype<T>? {
     return type as? Subtype<T>
}

func subtype<T>(of: T) -> Subtype<T> {  }     // Subtype(of: T) in the future?!

let b = B()
let asA: A = b
let hidden: Any = b

let asAType: Subtype<A> = subtype(of: asA)
let hiddenSubtype: Subtype<Any> = subtype(of: hidden)

dynamic(type: asAType, as: Any.self) //=> an Optional<Subtype<Any>>
dynamic(type: asAType, as: P.self)     //=> an Optional<Subtype<P>>
dynamic(type: asAType, as: R.self)    //=> an Optional<Subtype<R>>
dynamic(type: asAType, as: A.self)    //=> an Optional<Subtype<A>>
dynamic(type: asAType, as: B.self)    //=> an Optional<Subtype<B>>

dynamic(type: hiddenSubtype, as: Any.self) //=> an Optional<Subtype<Any>>
dynamic(type: hiddenSubtype, as: P.self)     //=> an Optional<Subtype<P>>
dynamic(type: hiddenSubtype, as: R.self)    //=> an Optional<Subtype<R>>
dynamic(type: hiddenSubtype, as: A.self)    //=> an Optional<Subtype<A>>
dynamic(type: hiddenSubtype, as: B.self)    //=> an Optional<Subtype<B>>

let rType = R.self
let exactAnyType: ExactType<Any> = rType //=> Error
let anyType: Subtype<Any> = rType              // fine

dynamic(type: anyType, as: Any.self) //=> an Optional<Subtype<Any>>
dynamic(type: anyType, as: P.self)     //=> an Optional<Subtype<P>>
dynamic(type: anyType, as: R.self)    //=> an Optional<Subtype<R>>

dynamic(type: rType, as: Any.self) //=> an Optional<Subtype<Any>>
dynamic(type: rType, as: P.self)     //=> an Optional<Subtype<P>>

let pType = P.self

dynamic(type: pType, as: R.self)     //=> nil
dynamic(type: pType, as: Any.self) //=> Optional<Subtype<Any>>

@beccadax
Copy link

beccadax commented Jul 25, 2016

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 through T.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 of T is written T.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:

protocol Proto {}
class Base {}
class Derived: Base, Proto {}

Derived.Type is a subtype of both Base.Type and Proto.Type. That means that Derived.self can be used anywhere a Derived.Type is called for.

Unfortunately, this simple picture is complicated by protocols. Proto.self is actually of type Proto.Protocol, not type Proto.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 of T.self. So when Proto is bound to a generic parameter P, P.Type is the same as Proto.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:

  • Moving the implementation of metatypes at least partly into the standard library.
  • Adding members available on all type instances for features like read-write reflection or memory layout information.
  • Conforming metatypes to protocols like Equatable or CustomStringConvertible.
  • Offering straightforward syntaxes for dynamic features like looking up types by name.

Proposed solution

We abolish .Type and .Protocol in favor of two generic-style syntaxes:

  • ExactType<T> is the concrete type of T.self. An ExactType<T> can only ever accept that one specific type, not any of its subtypes.
  • Subtype<T> is the supertype of all ExactTypes whose instances are subtypes of T. If T is a class, Subtype<T> would accept an ExactType for any of its subclasses. If T is a protocol, Subtype<T> would accept an ExactType for any conforming concrete type.

In this new notation, some of our existing standard library functions would have signatures like:

func unsafeBitCast<T, U>(_: T, to type: ExactType<U>) -> U
func sizeof<T>(_: ExactType<T>) -> Int
public func == (t0: Subtype<Any>?, t1: Subtype<Any>?) -> Bool
func type<T>(of: T) -> Subtype<T>

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 with ExactType and Subtype by renaming it to Subtype(of:). We believe this is clearer about both the type and meaning of the operation.

let instance: NSObject = NSString()
let class: Subtype<NSObject> = Subtype(of: instance)

print(class)    // => NSString

Future Directions

  • We could allow extensions on ExactType and perhaps on Subtype 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.
  • It may be possible to implement parts of ExactType as a fairly ordinary final class, moving code from the runtime into the standard library.
  • We could offer a Subtype(ofType: ExactType<T>, named: String) pseudo-initializer which would allow type-safe access to classes by name.
  • We could offer other reflection and dynamic features on ExactType and Subtype.
  • We could move the MemoryLayout members into ExactType (presumably prefixed), removing the rather artificial MemoryLayout enum.
  • Along with other generics enhancements, there may be a use for a Subprotocol<T> syntax for any protocol requiring conformance to protocol T.
  • It may make sense to have ExactType<T> include a Subtype typealias which maps to Subtype<T>.

@DevAndArtist
Copy link

@Anton3

I asked Brent and he's fine to be a third author. I mean it's just obvious after all this. :)

@DevAndArtist
Copy link

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