Why there is no subtyping relation between existential meta types?

I'm confused about subtyping (lack of one) in meta types of protocol existentials.

Consider, the following example:

class C {}
class D: C {}

let c: C = D()
let cType: C.Type = D.self // OK

protocol P {}
protocol Q: P {}
struct S: Q {}

let q: Q = S() // OK. S seems to be a subtype of existential for Q.
let qType: Q.Protocol = S.self // error: cannot convert value of type 'S.Type' to specified type 'Q.Protocol'

let p: P = q // OK. Existential for Q seems to be a subtype of existential for P.
let pType: P.Protocol = Q.self // error: cannot convert value of type 'Q.Protocol' to specified type 'P.Protocol'

My intuitive understanding of subtyping is relation is that if you can write

let x: A = ...
let y: B = x

then A is a subtype of B.

And subtyping of types is related to subtyping of meta-types: if A is a subtype of B, then A.Type is also a subtype of B.Type.

Is my understanding correct? Is there a more formal definition?

And I'm confused, because from that logic if follows that S.Type should be a subtype of Q.Protocol, which in turn should be a subtype of P.Protocol. But currently that's not the case.

There's a difference between Q.Type, which is "the type of all values conforming to Q", and Q.Protocol, which is "the type of the protocol Q". That is, Q.Protocol is not an existential metatype. That explains the S.self example.

Now, should Q.Protocol be a subtype of P.Protocol? That's a little less clear. Abstractly that might be useful: then you could say "I want to check the conformance of some protocol that refines P". But the language doesn't actually support that today, or really any other operations on protocol types, so for now there's not much point.

Current syntax is pretty confusing. I'm trying to think of P.Protocol, as (any P).Type, this makes things a little more logical. Consider this example with current syntax:

struct Z<T> { init(_ type: T.Type) {} }
let p: P.Protocol = P.self
let z = Z(p)
print(type(of: z)) // prints 'Z<P>'

So, despite of being of type P.Protocol, p is also recognized as being a value of T.Type for T being the type of the existential container for P.

In that sense, when discussing about A and B, in the previous post, I assume that A or B can be existential types as well.

No, this is incorrect. It means that there is an implicit conversion from A to B. It may be upcasting (say if A is a subclass of B). It may involve creating an existential box (if say B is a protocol and A conforms to it). It may be a bridging conversion (A can be bridged to B). It may be an allowed implicit conversion between tuples.

    let upair: (Int, Int) = (a: 10, b: 10)
    let lpair: (a: Int, b: Int) = upair

I'm probably missing some other cases as well.

@Varun_Gandhi, could you explain the difference between subtyping and implicit conversion? Do both exist in Swift?

Subtyping forms a partial order.

  1. T is a subtype of T. (reflexivity)
  2. if T is a subtype of U and U is a subtype of T, then T = U (anti-symmetry)
  3. if T is a subtype of U and U is a subtype of V, then T is a subtype of V (transitivity)

Swift has subtyping for classes.

On the other hand, the implicit conversions for tuples don't have this behavior. A labelled tuple is implicitly convertible to an unlabelled tuple and vice-versa, but two labelled tuples are not implicitly convertible to each other (breaking transitivity) and the labelled and unlabelled types are not equal (breaking anti-symmetry).

// Multiple conversions are not permitted, breaking transitivity
let (x, y): (Int, Int) = (x: 10, y: 10) // OK
let (a: a, b: b) = (x, y)               // OK
let (a: _, b: _) = (x: 10, y: 10)       // error: cannot convert value of type '(x: Int, y: Int)' to specified type '(a: Int, b: Int)'

// Prints different types, breaking anti-symmetry
print(type(of: (a: 0, b: 0)), type(of: (0, 0)))

So yes, Swift does have both. What Swift doesn't have (and sometimes people ask for this, but I sure hope we don't implement it) is user-defined implicit conversions.

I misspoke about bridging conversions earlier (they don't happen with :), but (arguably) they do have counter-intuitive behavior -- naively you would expect that let y: T = x would be the same as let y = x as T when both compile but that's not the case.

let y = String() as NSString // OK
let z: NSString = String()   // error

If you relax the idea a bit (to include as and not just :), then it would seem like String and NSString are subtypes of each other and hence equal (by anti-symmetry) but they're not equal (and hence interchangeable), they are "equal upto bridging".

1 Like
Terms of Service

Privacy Policy

Cookie Policy