Swift Associated Type Design Patterns

Bob Godwin
7 min readMay 16, 2018

--

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:

associatedtypeis a protocol generic placeholder for an unknown Concrete Typethat 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:

  • associatedtypewas 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 rich Type relationship between Types.
  • They help specify the precise and exact Type of an object within a protocol sub-typing without polluting the Type 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 6alerting 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 it
  • Shadow 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 a Type 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 and AnyObject 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.

Special Thanks:

He is a genius

Other Articles:

--

--