Swift: From Protocol to AssociatedType then Type Erasure
Motivation
We use classes to represent a symmetric operation, like Comparison, for example, if we want to write a generalized sort or binary search where we need to compare two elements , we end up this something as below:
We don’t know anything about an arbitrary instance of Ordered
yet. So if the method is not implemented by a subclass, well, there is nothing we can do other than trap. Now, this is the first sign that we are fighting the type system. And if we fail to recognize that, it is also where we start lying to ourselves. Because we brush the issue aside, telling ourselves as long as each subclass of Order implements precedes, we will be okay. Making it the subclass’s problem. So we go ahead and implement an example of Ordered
as below.
It got double value and we override precedes
to do the comparison. other
is just arbitrary Ordered and not a number. So we don’t know that other
has value property. We down-cast Other
to Number
to get to the right type to compare. It is a static type safety hole. Classes don’t let us express this crucial type relationship between the type of self
and type of Other
. It is a code smell. So any time we see a force down-cast in our code, it’s a good sign that some important type relationship has been lost, and often that’s due to classes for abstraction. Clearly, what we need is a better abstraction mechanism.
Better Abstraction Mechanism
A abstraction mechanism must have following properties:
- Doesn’t force to accept implicit sharing or lost type relationships
- Force to choose just one abstraction and do it at the time types are defined
- Doesn’t force to accept unwanted instance data or the associated initialization complexity
- Doesn’t leave ambiguity about what need to override.
And yes…!!! Protocol has all of these properties.
Don’t start with a class. Start with a protocol…!!!
Protocol-Oriented Programming in Swift
When Swift was made, it was made as the first protocol-oriented. Though Swift is great for object-oriented programming, but from the way for-loops and String literals work to the emphasis in the standard library on generics, at its hearts, Swift is protocol-oriented. There is a saying in Swift: “Don't start with class, start with protocol” . So let’s redo the binary search example with protocol:
Protocol Self Requirement
Once we have a Self-requirement to a protocol, it moves the protocol into a very different world, where the capabilities have a lot less overlap with classes.
- It stops being usable as a type
- Collections become homogeneous instead of heterogeneous
- An interaction between instances no longer implies an interaction between all model types.
- We trade dynamic polymorphism for static polymorphism, but, in return for that extra type information we are giving the compiler, it is more optimizable
Protocol Extension
In our current implementation, we need to implement precedes
method for each type. e.g:
Here comes the power of protocol. One implementation to rule them all by using a constrained extension on Ordered.
Let’s take constrained extension a step further
Protocol Associated Types:
By now we know what is Protocol Oriented Programming (POP). But POP without Protocol Associated Types (PAT) will never be completed.
associatedtype
is a protocol generic placeholder for an unknown Concrete Type
that requires concretisation on adoption at Compile time.
Protocol Associated Types (PAT)= Type Alias + Generics
At one stage of programming in Swift, many of us came across below error:
Swift does not allow us to use Generic
parameters in Protocol
. To bypass this limitation, Swift introduced Protocol Associated Type
. Below example shows how to use associatedType
in Protocol
Type Erasure
Though associatedType
solved one problem but it also introduce another problem. In Swift we can not use Protocol
with associatedType
as a Type. What it means is, if we set a variable type to ViewModelType,
Swift
compiler will show following error:
Type Erasure
is the only saviour here to this error. 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
Example
The below code snippet shows how to implement multi-sectioned heterogeneous TableViewCell using Unconstrained Type Erasure
If you download the project shared below and run the app, you will see the app has two models TextCellModel
and ImageCellModel
displayed using two table cells TextTableViewCell
and ImageTableViewCell
respectively. As the app has different models and cells under single TableView section, without TypeErasure
we would face following problems
- Self or Associated Type Requirement:
let items: [CellModel] = [a_text_model, a_image_mode] // Error explained above
CellModel
has associated type requirement to avoid spaghetti code while dequeueing TableCell. Without associated type requirement out code would be as:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let items = sections[indexPath.section].items
let item = items[indexPath.row] // More the model types, more the if-else
if item is TextCellModel {
// load TextTableViewCell
} else {
// load ImageTableViewCell
}
}
- Heterogeneous Array:
As TableView has different models and cells under same section, without TypeErasure
we had to populate different array for different model which will lead to lots of if-else. Instead we used as:
let imageCell = AnyCell(ImageCellModel("cyclamen"))
let quoteCell = AnyCell(TextCellModel("Hello World.", author: "-"))
let anyCells = [imageCell, quoteCell]
if we had just directly instantiated our ImageCellModel
instance using the ImageCellModel
initializer, it would be of type ImageCellModel
. But because we have instantiated using this AnyCell
wrapper class, ImageCellModel
is now instantiated as type AnyCell
. We have just erased type information (this is what type erasure means)
Wrapper classes are conventionally prefixed with the word “Any”, in order to guarantee that you will instantiate an object that implements our protocol and fills the generic type, without necessarily having the implementation on hand.
🎉 Using AnyCell
wrapper we’ve erased the Type
requirement when conforming to CellModel
protocol. The init
function is without clause and we now have a heterogenous collection Type
and dynamic dispatch at our disposition. 👍🏿
Source Code:
Related Articles:
Swift Associated Type Design Patterns
Protocol-Oriented Programming in Swift
Swift: Attempting to Understand Type Erasure
Conclusion
We often end up writing spaghetti code while implementing TableView or CollectionView with different types of cells and models. TypeErasure
is the right choice to avoid spaghetti code, makes code much more organized and increase readability. I hope that you have enjoyed this article. I encourage you to read my other articles Custom UIView from .xib and TableView Prefetching DataSource using Swift
Thank you all for your attention 🙏🏻. feel free to tweet and get connected.
Disclaimer: I went through bunch of other articles and copied easy to understand examples and sentences while writing this article