Protocol extension static method generic selection question

I found the behavior demonstrated below very unintuitive, and am wondering if someone could help me understand why Swift works this way. Consider this code:

protocol Animal {
  static var type: String { get }
}

extension Animal {
  static func greeting() -> String {
    return "I am an animal of type: \(type)"
  }
}

struct Cat: Animal {
 static var type = "Cat"

 static func greeting() -> String {
    return "Meow, I'm a cute Cat"
  }
}

func greet<A: Animal>(_ AnimalType: A.Type) {
  print(AnimalType.greeting())
}

greet(Cat.self) // 🧐 what will this line print ???

My intuition was that the line marked would print "Meow, I'm a cute Cat" , but in fact it prints "I am an animal of type: Cat" , with Swift for some reason choosing the generic, fallback method supplied as a protocol extension instead of the specific implementation provided by the conforming type. Turns out the fix is easy, which is to add this line:

protocol Animal {
  static var type: String { get }
  static func greeting() -> String // πŸ‘‹ πŸŽ‰ this fixes the issue
}

However, I thought that by not making the function part of the protocol I was creating an "extension point" for conforming types. But it seems like in this case, I have to make it a protocol requirement so that Swift picks the right method.

I'm sure I've got some sort of gap or blind spot in my understanding about how this works. I'm hoping someone will be able to explain the thought process behind the implementation here, maybe if it has a specific name, or point me to some part or parts of the language handbook, or blog posts so I can learn more.

Hello. this is interesting case, though not obvious. I recommend you to read some materials about method dispatch in Swift.
What is happening here is:

1)
let cat: Cat.Type = Cat.self
cat.greeting() // prints "Meow, I'm a cute Cat", because concrete Type is used

2)
let animal: Animal.Type = Cat.self
animal.greeting() // prints "I am an animal of type: Cat", because protocol type is used. 
// In other words, in runtime only method from protocol extension is known. 
// Even our Cat has the same method, it can be not known in runtime, because there is no information in witness table about it. 
// Imagine you have a Dog without its own greeting() method. So, only method form protocol extension is guaranteed to exist.

3) 
In generic context, such as func greet<A: Animal>(_ AnimalType: A.Type), `A` type is known 
to conform Animal protocol, the concrete type is unknown. 
So the behavior is the same as in case 2)

Yes, this fixes, because of method dispatch rule. Now greeting() method is added to protocol requirements. So in runtime we can prove that any implementation (concrete type) has this method. We can look up witness table and call the method, implemented in concrete type

2 Likes

Thanks for the reply. I think I mostly understand what you're saying, although I'm a little fuzzy on witness tables. But I'm wondering if you (or someone else) could clarify one thing. You say, that for this function:

func greet<A: Animal>(_ AnimalType: A.Type) {
  print(AnimalType.greeting())
}

...that the concrete type is unknown to the compiler. I thought the concrete type would be known, because I'm using the generic as a constraint not as the type. If i had written it as:

func greet(_ AnimalType: Animal.Type) {
  print(AminalType.greeting())
}

...without the generic constraint, I would have expected Swift to lose all of the concrete type info.

This is a little bit tricky. In generic function we use protocol only as generic constraint, and we might expect that after generic specialization pass our generic type will be be transformed to a concrete Type. And, if so, the greeting function of that concrete type will be called. But it is not.
Firstly, concrete type can not be always specialized. In this post it is explained: SE-0309: Unlock existential types for all protocols - #158 by Joe_Groff
Secondly, all we know in generic context about a Type is information from generic constraints. Some concrete types might have the same methods as in protocol extension. But others don't. So in generic context we can only rely on what is available in protocol, not in concrete type.

func greet(_ AnimalType: Animal.Type) - this is not a generic function, but it will print the same.

The main point here is that you need to add this method in protocol requirements, not in extension. Then even in generic context or in protocol type it can be proven, that this method exists in all concrete types and can be found in witness table.

1 Like

Ok, thanks again, that's really helpful, and I appreciate the link to the post with more info. :+1:

The key here is that in Swift (unlike, say, C++), generic specialization is optional. Sometimes the optimizer can specialize, and sometimes it will, sometimes it cannot. But of course, whether a method is specialized or not based on optimizer decisions should never change what the method does.

One of the consequences of this is methods in other frameworks can be generic without needing to expose the full implementation to the caller. If Swift specialized everything, it would need access to the full source all the way down. This is fine for a lightweight thing like a generic algorithm from the swift-algorithms package, but not for a generic method in an ABI-stable framework like SwiftUI.

If method dispatch were based on all extensions, and extensions can be made on any type from any framework, that would have the consequence that every type would need a global table where all the methods were put, and dispatch would be a matter of looking up the method in that table. That’s certainly something that can work – Objective-C works like this. But it gives you a very different language with significantly different trade-offs (most of them bad… though some of that claim is subjective).

4 Likes

Thanks for more details o this topic, Ben