Yes, this is the distinction. There is a difference between protocol dispatch and overloading.
Protocol Dispatch
-
When you define a protocol, what you are defining is a table of functions that need to be filled in by the type that conforms to the protocol. In CustomStringConvertible's case, it's a table with just one single function to get the description property.
-
When you conform to a protocol, you create that table and fill it in. So Set conforms to CustomStringConvertible and fills it in with its copy of description. Once filled in, at the point Set is compiled, that table cannot be altered. And a type can have one and only one table for a protocol conformance. It can't have multiple tables depending on different circumstances like different conditional conformance, or multiple protocols that further refine the original protocol.
-
When you write generic code constrained to a protocol, you are writing code that takes that table and uses it. It looks up the table for the passed-in type and then calls the functions it finds in that table. Since you cannot alter the table, nothing you do outside that generic function can change the function that is going to be called.
So, once Set has conformed to CustomStringConvertible there is nothing you can do that will change the version of description that will be used inside a function that uses that protocol conformance.
Overloading
Swift supports overloading. You can write two versions of a function, both with the same name, and when you call that function, Swift will pick which version to use. The rule of thumb for which one it will call is "the most specific" one. So for example:
// least specific – could be any T
func f<T>(_ t: T) { print(1) }
// slightly more specific, any T that conforms to CustomStringConvertible
func f<T: CustomStringConvertible>(_ t: T) { print(2) }
// more specific still: takes a Set, but containing any T
func f<T>(_ t: Set<T>) { print(3) }
// very specific: takes only a Set of Int
func f(_ t: Set<Int>) { print(4) }
The choice of which function gets called is made at the call site, at compile time. When you call a function, the compiler takes all the implementations of a function it can see at the time of compilation, ranks them, and picks the most specific one that will work:
let intset: Set = [1,2,3]
f(intset) // prints 4
let stringset: Set = ["a", "b"]
f(stringset) // prints 3
f("foo") // prints 2, String conforms to CustomStringConvertible
let tuple = (1,2) // tuples don't conform to protocols
f(tuple) // so this prints 1, the only option
Protocols and Overloads Together
Now suppose we write another function, g, that is generic:
func g<T: CustomStringConvertible>(_ t: T) {
f(t)
}
g(intset) // prints 2
Why does this print 2? Why not 4?
It does so because within the function g, all that is known about T is that it is some type that conforms to CustomStringConvertible. But what type is not known. So, it calls the most specific function f that works for that set of constraints. Outside of g, it might be known that t happens to be a Set of Int, and so there is a more specific overload for that. But that information cannot be used inside g, which only knows about what it is told via its constraints.
Why can't g consider overloads beyond that? Well, from a practical perspective, the compilation and runtime for that kind of system would be way more complicated, and near-impossible to optimize. Swift would need to keep tables for every type with every possible overload. When taking type-based overloading into account, this would end up being far more complicated equivalent of Objective C's message sending. And everything would need to happen at runtime, because you can't know ahead of time if a library is loaded into an app that defined another method. This rules out most specialization and optimization (which types such as Set benefits from for efficient operation) entirely.
But more importantly, arbitrary code injection into functions is very much a non-goal in Swift. As powerful as it is, being able to override fundamental behaviors of a type is dangerous when applied to types that have not explicitly been designed to account for that possibility (and in a world where anything can be monkey patched, no types would reasonably be able to account for that possibility).
Even without the framework issue, the behavior of g above is a good thing for local reasoning. In a world where specific overloads were taken into account, knowing exactly what function would be called based on what type is passed in would be very hard to follow. Instead, you have a simple rule for which description is going to be called: the one the type gained when it conformed to CustomStringConvertible, every time.