Swift: Accomplishing Dynamic Dispatch on PATs (Protocol with Associated Types)
Protocol-Oriented Programming is awesome and powerful, we have no doubt about it! However Swift is still yet to be perfect (at least at the time this article’s being written), and one of the shortcoming of Swift that I want to talk about today is not able to perform dynamic dispatch when using PATs (Protocol with Associated Types) and how we can workaround this shortcoming and make PATs support dynamic dispatch.
What are PATs?
Associated type is a placeholder for an unknown type, and PATs are protocol that have one or more associated type requirement.
Animal
protocol as shown below is an example of PATs.
If you would like to know more about PATs, I recommend you to read this awesome article by NatashaTheRobot.
What is Dynamic Dispatch?
Dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at run time. — wikipedia
Sample code below demonstrate how we can achieve dynamic dispatch using protocol (without associated type).
Code above will generate 2 lines of output:
My Tiger is walking in the jungle.
My Cow is walking in the farm.
This shows that the walk()
function calls have been dynamically dispatched to the desire concrete type implementation.
The Shortcoming of PATs
To demonstrate the shortcoming of PATs, let’s add an associated type requirement FoodType
to our Animal
protocol. We will also introduce eat(food: FoodType)
function requirement to the Animal
protocol.
If you paste the sample code above to your Xcode playground, you will get an error as shown below.
error: Protocol ‘Animal’ can only be used as a generic constraint because it has Self or associated type requirements
What does this mean? It basically means that we can’t use our Animal
protocol as type anymore if our Animal
protocol contains one or more associated type requirements.
But why??? 😢
Let’s take a look at the code snippet below.
Logically, we can say that code above is correct because we are feeding Meat
to Tiger
and Grass
to Cow
. However, there is no way for the compiler to know whether we are passing the correct FoodType
type into the eat
function. Both myTiger
and myCow
are Animal
type, therefore the compiler have no idea which one is Cow
type and which one is Tiger
type. This kind of ambiguity explained why the compiler is stopping us from using Animal
as type. Thus making dynamic dispatch impossible in this case.
The Workaround 💡
To workaround this we will use a technique called type erasure to hide dynamic dispatch within a type. To know more about type erasure, here’s an awesome talk by gwendolyn weston that you should not miss!
Back to our workaround, what we will do is stop using Animal
protocol as type and introduce another new concrete type to help us on performing dynamic dispatch. Here we will name our new concrete type AnyAnimal
, it is an enum that conform to the Animal
protocol.
The code above is quite self explanatory, you can imagine that the AnyAnimal
enum is a dispatcher that in charge of dispatching a function call to the desire Animal
concrete type.
Do note that conforming AnyAnimal
to Animal
protocol is not mandatory here, however by doing so, we can reduce the robustness of our code. Imagine situation where new function requirement being added to Animal
protocol in future, compilation error will trigger if we forget to update AnyAnimal
protocol. Whereas no error will trigger if we do not conform AnyAnimal
to Animal
protocol.
Enough said! Let’s see AnyAnimal
enum in action…
In code snippet above, we use AnyAnimal
to replace Animal
as our animalArray
element type. With this we can avoid using Animal
as type thus avoiding the error we previously encountered.
Next, let’s take a look at the output we get by enumerating each element in animalArray
.
My Tiger is walking in the jungle.
My Tiger eat Meat
My Cow is walking in the farm.
My Cow eat Grass
We can see that the AnyAnimal
enum is dispatching walk()
and eat(food: FoodType)
function call to the desire Animal
concrete type correctly. 🎉 🎉
If you want to try out the code in your Xcode playground, you can get the full example code here.
Conclusion
The workaround in this article might not be ideal as it contain a lot of boilerplate code and we will have to handle all the dispatching logic manually. However, until the day Swift support existential, enums and type erasure might be the best tools that we can use to accomplish dynamic dispatch on PATs.
[Update]: 24 Oct 2017
Thanks to Hacker News user skue suggestion, we can actually improve AnyAnimal
enum by changing it to be a generic struct.
With above generic struct, we will not need to update AnyAnimal
manually every time when we add a new class that conform to Animal
protocol. 👏👏👏
Further Reading
Thank you for taking your precious time to read this article. Please do not hesitate to hit the 👏 button or share this article if you like it. If you have any comments or questions, just drop it in the comment section below.
Feel free to follow me on Medium or Twitter if you want to read more article like this in future.