Swift Associated Type Design Patterns
Swift is a multi paradigm programming language, which means you can do object-oriented, aspect-oriented, procedural, functional or pop. Just to mention a few. The very last one “pop” means protocol-oriented programming. Everything changed in this session of WWDC 2015 where Dave Abrahams presented a talk on this concept and new way of thinking. He started by saying :
New way of thinking:
The next 40 minutes are about putting aside your usual way of thinking about programming. What we’re going to do together here won’t necessarily be easy, but I promise you if you stick with me, that it’ll be worth your time.
I do solemnly advice you to watch the video now if you haven’t seen it. Because what I am about to do in this article is to break down the same video.
In the same year Alexis Gallagher presented a speech where he tried to address some difficulties encountered while working with Associated Types in swift programming language. This is not an easy concept to understand, Benedikt Terhechte wrote on this topic, Russ Bishop also wrote his memoir on Associated Types, John Sundell also paid his tribute to Associated Types. Robert Edwards gave his break down on type erasure, Lee kah seng wrote his findings on how to attain dynamic dispatch while working with Protocol Associated Types. They were all trying to understand how parametric polymorphism works.
The beauty of annoying error message:
protocol can only be used as a generic constraint because it has self or associated type.
Before you get annoyed and start smashing the keyboards when ever you see the above mentioned error. Let’s define exactly what is an associated type.
Definition of Associated Types:
associatedtype
is a protocol generic placeholder for an unknown Concrete Type
that requires concretisation on adoption at Compile time.
Clarity on Compile time vs Runtime:
Runtime and Compile time are programming terms that refer to different stages of a software application. Compile time is the instance where the code being is converted into an executable code while Runtime is the instance where the converted executable code is actually in execution.
Origin of Associated Types:
This concept first appeared in a publication from “The Journal of Functional Programming” titled: extended comparative study of language support for generic programming. Where they laid emphasis on Multi-Type Concepts which is the root of Swift’s protocol associatedtypes
. Swift also drew some inspiration from Scala’s Traits and Abstract types, Haskell’s Multi-parameter type classes and from Rust Associated Types. It then leveraged the Multi-Type Concept within the standard library for it’s collection types.
Problems solved by Associated Types:
associatedtype
was introduced to solve the problem of rich multi type abstraction which are not available in object-oriented subtyping.- Designed to address the known naive
generic protocol
implementations especially where complexity scales badly with the increase of generic types introduction. - Maintain static type-safety while making the language more expressive.
Advantages of Associated Types:
- They help avoid implementation details leakage, that are normally required to be specified repeatedly.
associatedtype
captures the richType
relationship betweenTypes
.- They help specify the precise and exact
Type
of an object within a protocol sub-typing without polluting theType
definition. - They provide the relationship that you cannot fit into an object related type hierarchy, most especially where the Liskov substitution principle fails to address the
Type
polymorphic relation. - They enforce a homogeneous collection which boosts the compiler with optimised statically dispatch codes.
Caveats of Associated Types:
- Difficult to understand because it comes with a high learning curve.
- It tends to lock you out of Dynamic dispatch. By enforcing a Static dispatch.
- It can only be used within protocols.
Clarity on dynamic dispatch vs static dispatch:
Dynamic dispatch is the process of selecting which implementation of a polymorphic operation (method or function) to call at Runtime while Static dispatch is a fully resolved Compile time form polymorphic operation.
Working with Associated Types:
Declaring a protocol with associated types is pretty straight forward and as we can see in the example below:
🤗 We can easily implement the protocol adoption or better said in Cocoa terms conform
to the protocol as follows:
🍱 Let’s make a clear distinction of what is an associatedType
. As said earlier they are generic placeholders but not a generic type. You can also refer to it as parametric polymorphism.
👉🏿 Take a look 👀:
In the above implementation the app will trap at #line 6
alerting the following error:
Cannot convert value of type ‘ExtendedDetail’ to expected argument type ‘Cell.T’ (aka ‘Detail’)
This is because on adoption or conformation to the protocol we are required to specify the Concrete Type
and we did that by saying typealias T = Detail
so therefore our function already knows at Compile time the Concrete Type
to expect and that’s why it raises an exception if we try to use ExtendendDetail
instead of Detail
.
🍻 Let’s add the implementation of ExtendedCell
which conforms to the same protocol but using a different Concrete Type
🤷🏽♂️ Taking it further, if we naively decide to create a collection of TableViewCell
as shown in the code above then the beautiful error message will be triggered at #line 27
🙈:
Protocol ‘TableViewCell’ can only be used as a generic constraint because it has Self or associated type requirements
🧚🏽♂️ The only saviour here to this error is called type erasure, but before we jump into it let’s take a look at what this term means?
Type Erasure Definition:
Type erasure refers to the compile-time process by which explicit type annotations are removed from a program, before it is executed at run-time.
There are three patterns that we can apply to solve the problem of generic constraints requirement.
Constrained Type Erasure:
erases a type but keeps a constrain on it.Unconstrained Type Erasure:
erases a type without a constrain on itShadow Type Erasure:
erases a type by camouflaging the type
Constrained Type Erasure:
This pattern adds an initializer constraint on the wrapper class
in order to guarantee that the injected generic type
matches with the associatedtype
💪🏾 In the above code we’ve used the AnyRow
to erase the Type
requirement when conforming to Row
protocol. Taking a keen look at #line 20
we see that there is constraint on the init
function using the clause: where T.Model == I
This also locks us into a homogeneous collection Type
as shown on #line 64
Unconstrained Type Erasure:
This pattern comes to our rescue if we want to have a heterogeneous collection Type
and Swift language provides two special Types
for working with nonspecific Types
the Any
& AnyObject
Any
can represent an instance of any type at all, including function types.AnyObject
can represent an instance of any class type.
With this information let’s implement a type erasure that can give us heterogenous collection using the Any
as indicated.
🎉 With the help AnyCellRow
wrapper we’ve erased the Type
requirement when conforming to Row
protocol. The init
function is without a clause and we now have a heterogenous collection Type
and dynamic dispatch at our disposition. 👍🏿
Shadow Type Erasure:
In order to implement the shadow type erasure we need to add another protocol and refactor the Row
protocol as follows:
The next step is to add a default implementation using swift extension
for the Row
protocol functions and properties.
👏🏾 With that we can now hide behind the scene and also be able to use TableRow
as first class citizen as shown below:
Summary:
These patterns defer from each other and they yields different results.
- The first is the mostly optimised because your collection will be inlined by the swift compiler for static dispatch.
- The second makes it possible for us to have dynamic dispatch and heterogenous collection, but there is a caveat there because
AnyCellRow
could be instantiated with any kindType
even with aType
that isn’t related to what we are working on. - The last one seems to cut in between but tends to be verbose and repetitive if you have a large code base with lots of protocols.
Warning from Swift on Any and AnyObject:
Use
Any
andAnyObject
only when you explicitly need the behavior and capabilities they provide. It is always better to be specific about the types you expect to work with in your code.
After this message, I did say it’s up to you to choose what suites best depending on the situation you find yourself in. One thing to keep in mind is that Any
can tricky in some cases. The Any
type represents values of any type, including optional types. But when queried it will only return true
in down casting if the object is not nil
. For more on this please consult Swift Type Casting
You can find code samples discussed in this article at my GitHub Playground repository
Thanks for reading, I hope I was able to entertain you. I like blogging on Swift language so feel free to reach out on twitter if there is anything particular you want me write about.