Skip to content

Instantly share code, notes, and snippets.

@xwu

xwu/metatypes.md Secret

Last active September 30, 2016 21:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xwu/0cc2c8d358f1fdf066ba739bcd151167 to your computer and use it in GitHub Desktop.
Save xwu/0cc2c8d358f1fdf066ba739bcd151167 to your computer and use it in GitHub Desktop.
Metatypes in Swift
Struct or enum Class Protocol
struct S { } (A) class Base { } (B) protocol P { } (C)
class Derived : Base { } (D) struct Impl : P { } (E)

Currently, given func foo<T>(_ bar: T.Type) {...}, T.Type is one of (A), (B), (C) or (D) but not (E). A way of referring to (A), (B), (D) and (E) is desired.

The authors propose, in the same context, the following:

  • Type<T> is (A), (B) and (C)
  • Subtype<T> is (A), (B), (D), (E)

Without bikeshedding names, what is the utility of having both Type and Subtype, which distinguish between (B) and (D)? The answer cannot be for exact matching, because it is possible to perform exact matching without the proposed Type.

That is, given an instance d of type Derived, it is currently the case that type(of: d) != Base.self. In the proposed syntax, this would be subtype(of: d) != Base.self.

An alternative, simpler design which would address the stated motivating issues would be:

  • Type<T> is (A), (B), (D), (E)
  • Protocol<T> is (C)

What benefits of distinguishing Type and Subtype cannot be captured by this simpler design?

@DevAndArtist
Copy link

I though I made it crystal clear in my last post what the main problem is.

Again:

T.Type serves two jobs at once.

  1. It's a concrete metatype of T.
  2. It's an existential metatype of T where other metatypes where U is a subtype of T (U : T) are subtypes of this existential metatype.

Forget about protocols for a moment:

struct S { } 

let metatype_1: Any.Type = S.self // <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

let metatype_s: S.Type = S.self // <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

/*
   The relationship looks here like this

   (concrete metatype) `T.Type : T.Type` (existential metatype) 

   OR for the given example: `S.Type : S.Type : Any.Type` (last one is again an existential metatype)
   This looks confusing right?
*/

class B { }
class D : B { }

let metatype_b: B.Type = B.self // <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

metatype_b is D.Type // false

let metatype_d: D.Type = D.self // <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

let metatype_2: B.Type = metatype_d   // Totally fine
let metatype_3: Any.Type = metatype_2 // Okay

metatype_3 is D.Type // true

/*
   Relationship:

   (existential metatype) `B.Type : Any.Type` (existential metatype)
   (concrete metatype) `B.Type : B.Type` (existential metatype)

   (existential metatype) `D.Type : B.Type` (existential metatype)
   (concrete metatype) `D.Type : D.Type` (existential metatype)
*/

It should be clear by now that there is this odd T.Type : T.Type relationship. We want to correct this behaviour + solve the problem that raises with protocols with one simple and single design.

Let's see what happens with protocols:

protocol P { } 

let metatype_p: P.Type = P.self // Error, because the concrete metatype is not a subtype of the existential metatype of P

// Furthermore `P.self` is `P.Protocol`

let metatype_3: Any.Type = P.self // fine <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

/*
   Relationship:
   (concrete metatype) `P.Protocol : Any.Type` (existential metatype)
   (existential metatype) `P.Type : Any.Type` (existential metatype)

   At this time `P.Type : Any.Type` is an existential metatype that exists but it does not have any subtypes!
*/

struct I : P { }

let metatype_i: I.Type = I.self // <~~ (concrete) metatype
// ~~~~~~~~~~~~ ^ existential metatype

let metatype_4: P.Type = metatype_i // fine
// ~~~~~~~~~~~~ ^ existential metatype

metatype_4 is I.Type // true

/*
   Relationship:
   (existential metatype) `P.Type : Any.Type` (existential metatype)
   (existential metatype) `I.Type : P.Type` (existential metatype)
   (concrete metatype) `I.Type : I.Type` (existential metatype)
*/

There is a huge overlap in the current design. I hope this cleared your question here.

Side note: The following function isn't possible to implement with the current T.Type design because in generic context a protocol will end up T.Protocol.

func dynamic<T>(subtype: Subtype<Any>, `is` _: Type<T>) -> Bool {
  return subtype is Subtype<T>
}

The proposed design however solves these problems and the relationship becomes clearer:

(existential metatype) `Subtype<B> : Subtype<Any>` (existential metatype)
(concrete metatype) `Type<B> : Subtype<B>` (existential metatype)

(existential metatype) `Subtype<D> : Subtype<B>` (existential metatype)
(concrete metatype) `Type<D> : Subtype<D>` (existential metatype)

(existential metatype) `Subtype<P> : Subtype<Any>` (existential metatype)
(concrete metatype) `Type<P> : Subtype<Any>` (existential metatype)

(existential metatype) `Subtype<P> : Subtype<Any>` (existential metatype)
(existential metatype) `Subtype<I> : Subtype<P>` (existential metatype)
(concrete metatype) `Type<I> : Subtype<I>` (existential metatype)

The only way to work with Subtype<T> is by using subtype(of:) function of by manually shadowing a concrete metatype Type<T>.

The only way to instantiate a concrete metatype is done with T.self.

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