Previously, we looked at how Swift functions can be overloaded just by return type, and how Swift picks between different possible overloads based on a best-match mechanism.
All this was working towards the reason why 1...5
returns a Range
rather than a ClosedInterval
. And to understand that, we’ll have to look at how generics fit into the matching criteria.
But before we do, a brief diversion into protocol composition.
Protocols can be composed by putting zero1 or more protocols between the angle brackets of a protocol
<> statement. The Apple Swift book says:
NOTE
Protocol compositions do not define a new, permanent protocol type. Rather, they define a temporary local protocol that has the combined requirements of all protocols in the composition.
Reading that, you might think that declaring protocol
<P,Q
> was essentially like declaring an anonymous protocol Tmp: P, Q { }
, and then using Tmp
, without ever giving it a name. But that’s not quite what this means, and a way to spot the difference is by declaring functions that take arguments declared with protocol
.
First, a recap of some of the behaviours of regular protocols. Suppose we define 4 protocols: first P
and Q
, and then two more, A
and B
that both just conform to P
and Q
:
protocol P { } protocol Q { } protocol A: P, Q { } protocol B: P, Q { }
Although A
and B
are functionally identical, they are two different protocols. This means you can write two functions, one that takes A
and one that takes B
, and they’ll be two different functions with equal priority, so if you try to call them and either one would work, you’ll get an ambiguous call error:
struct S: A, B { } let s = S() func f(a: A) { print("A") } func f(b: B) { print("B") } // error: Ambiguous use of 'f' f(s)
If, on the other hand, we use protocol
<P,Q
> instead of B
, we get a different result:
func g(a: A) { print("A") } func g(p: protocol<P,Q>) { print("protocol<P,Q>") } // prints "A" g(s)
If protocol
<P,Q
> were really declaring a new anonymous protocol that conformed to P
and Q
, we’d get the same error as before. Instead, its like saying “an argument that conforms to both P
and Q
”.
As we saw in the last article, an inherited protocol (in this case A
) always trumps its ancestors (in this case P
and Q
). So the A
version of g
is called. This is consistent with our rule of thumb, that Swift will always pick the function with more “specific” arguments – here, inheriting protocols are more specific than inherited ones.
Another way to be more specific is to require more protocols. Here’s an example of how three protocols is more specific than two:
protocol R { } extension S: R { } // you can assign an alias for any // protocol<> definition if you prefer typealias Two = protocol<P,Q> typealias Three = protocol<P,Q,R> func h(p: Two) { print("Two") } func h(p: Three) { print("Three") } // prints "Three" h(s) // you can still force the other to be // called if you want: this prints "Two" h(s as Two)
Finally, if given a choice between an inheriting protocol, or more protocols (i.e. depth vs breadth), Swift won’t favour one over the other:
func o(p: protocol<A,B>) { print("A and B") } func o(p: protocol<P,Q,R>) { print("P, Q and R") } // error: Ambiguous use of 'o' o(s) // prints "A and B" o(s as protocol<A,B>) // prints "P, Q and R" o(s as protocol<P,Q,R>)
One of the conclusions here is that there are quite a few possible ways for you to declare functions that take the same argument type, and to tweak the declarations to favour one function being called over another. When you combine these with different return values, that gives you a tool to nudge Swift into inferring a specific type by default, but allowing the user to get a different one from the same function name if they prefer, while avoiding forcing the user to deal with ambiguity errors. This is what is happening with ...
when it defaults to Range
.
So now, on to generics, which will take this even further.
- zero being an interesting case we’ll look at some other time ↩
[…] how different functions with simple single arguments were picked on a “best match” basis, and part 3 went into a little more detail about […]
[…] “Which function does Swift call? Part 3: Protocol Composition” […]