Small question about type comparison

Hi there, at very beginning I want to say sorry for my English as I am not native speaker :)
Today I found quite interesting behaviour which made me confused. I have defined 2 types + one extension in my code while implementing feature to app:

protocol TypeA {
func test()
}

class TypeB: TypeA {
func test() { }
}

extension Optional {
typealias WrappedType = Wrapped
}

TypeA and extension for Optional are defined in framework included to app project where TypeB is defined.

Then I wanted to do some type comparison to find correct object from array, thats when I found interesting behaviour. Here are 4 checks I made with their results:

  1. TypeB.self is TypeA.Type; result is true
  2. TypeB.self is Optional< TypeA >.WrappedType.Type; result is false
  3. Optional< TypeB >.WrappedType.self is Optional< TypeA >.WrappedType.Type; result is false
  4. Optional< TypeA >.WrappedType.self == TypeA.self; result is true

Now there is my question:
Why are checks from points 2 and 3 failing? From my understanding typealias WrappedType is just exposure of Optional's generic type Wrapped, which, I believe, should equal TypeA. It kind of bothers me because I am wondering if it is a bug where TypeA != TypeA or there is reason why check 1 and 2 aren't equal. Am I just not aware of some trait of swift?

I will be grateful for any response for that question and sorry if I made topic in wrong section :)

2 Likes

I believe what you're running into here is the difference between TypeA.Type and TypeA.Protocol. These are subtly different and unfortunately once generics are involved it is possible to write in code a type reference that looks like it should resolve to TypeA.Type when really it resolves to TypeA.Protocol.

The difference is a little bit hard to describe, but in short:

  • TypeA.Type refers to what is called the "existential metatype," which is the supertype of all types which conform to TypeA.
  • TypeA.Protocol refers to the "protocol metatype", which is the type of TypeA.self.

The (natural) language we have to describe all of this is... not great and I have talked myself in circles about it before. You can read this thread which contains what might be the most thorough exploration of .Type vs. .Protocol to date, though I'm not sure you'll come out of that thread less confused than going in. :smile:

It might be easiest to think about this in terms of examples. Because TypeA.Type is the supertype of all conforming types, there are things you can do with a value of type TypeA.Type that you can't do with a value of type TypeA.Protocol. For example, if TypeA has a static requirement, we can call that requirement on a value of type TypeA.Type:

protocol TypeA {
  static func test() -> Int
}

class TypeB: TypeA {
  static func test() -> Int { 0 }
}

func f (_ t: TypeA.Type) {
  print(t.test()) // 0
}

Because the underlying value of t must be some type which conforms to TypeA, we know that there must be an implementation of test() available for us to invoke. OTOH, the value TypeA.self has no such implementation because TypeA only specifies the requirement that conforming types must implement. So it is not possible to call test() on a value of type TypeA.Protocol.

The last piece of the puzzle here which I mentioned briefly above, is that in generic contexts, T.Type refers to T.Protocol when T is bound to an existential type, because we want it to be the case that T.self is always of type T.Type. (Notably, it is not the case that TypeA.self is TypeA.Type, but we sacrifice this in concrete contexts on the assumption that when people write TypeA.Type what they usually want is "some meta type which conforms to TypeA."

This extra wrinkle can result in some pretty confusing behavior, and I'm not actually certain whether the behavior you've identified here should be classified as a bug or not. We have all the concrete information needed to know that Optional<TypeA>.WrappedType is TypeA, so it's a little strange that the usual rule for generic meta type resolution would kick in. Maybe there's a reason for this, though.

5 Likes

Thanks for your answer! :slight_smile: I suspected it probably is related to existance of TypeA.Protocol but I got a little confused as debugger continued to show TypeA.Type instead of TypeA.Protocol, as variable type, while I evaluated expressions during case 2. Thanks to your answer now I have better understanding of difference between TypeA.Type and TypeA.Protocol. I for sure will read pitch you linked, seems like nice read for Firday evening :smiley:

Thank you, @Jumhyn.

After reading that thread, I still feel that I am missing something about the significance of putting the any keyword before the existential type.

For example, given this protocol:

protocol Foo {
   ...
}

I don't understand what additional information the presence of any provides in this declaration:

let foo: any Foo

The following declaration is shorter, and smells just as sweet. :slight_smile:

let foo: Foo

I really need a killer example to wrap my head around this.

The main thing that I've seen, from other threads, is that any Foo is heavier than some Foo at runtime, so to speak, especially if the underlying type is a struct rather than a class. There's a thread somewhere about making the default some rather than any in Swift 6.

I'm not sure I'll be able to come up with any better examples than what's discussed in that thread, but perhaps I'll be able to phrase things differently on another attempt.

The information that is intended to be provided by any Foo as opposed to Foo is emphasis on the fact that the underlying dynamic type of foo will be one of (potentially many) types that conform to Foo, which in some cases prevents us from doing things with values of type any Foo that it seems like we should be able to do from a naive look at the Foo protocol.

Before any was introduced, we used the bare protocol name Foo to talk about two distinct things:

  • The set of functionality that all types conforming to Foo share (as in func f<T: P>(_ t: T)).
  • The type of a variable which can hold any P-conforming instance (as in let foo: Foo above).

But consider if Foo were defined as follows:

protocol Foo {
  func doSomething(with other: Self)
}

This protocol would be conformed to by providing an implementation of doSomething(with:) that accepts a value of the same type as the conforming type. So a structs R and S would conform with different signatures for doSomething(with:) as:

struct S: Foo {
  func doSomething(with other: S) {
  }
}

struct R: Foo {
  func doSomething(with other: R) {
  }
}

But now suppose we had the following function:

func makeFoo(useS: Bool) -> Foo {
  if useS {
    return S()
  } else {
    return R()
  }
}

What happens if we try to do

makeFoo(useS: true).doSomething(with: makeFoo(useS: false)

?

An quick look at the protocol Foo might suggest this should work—after all, the (static) type of makeFoo(useS:) is always Foo, and doSomething(with:) accepts a value of type Self, so why shouldn't it work to pass a value of type Foo to doSomething(with:).

But as you can see from the setup, there's no implementation which we could possibly invoke for thus use of doSomething(with:). S.doSomething(with:) only accepts values of type S, and makeFoo(useS: false) is going to give us a value of (dynamic) type R. So this must fail to compile.

By writing makeFoo(useS:) as returning any Foo, it is (hopefully) a bit clearer why the failure happens here. The function doesn't just produce a value of type Foo, it produces a value of 'any' type that conforms to Foo—which means that any Foo may not actually satisfy the requirements needed to conform to Foo.

6 Likes