How to make the best of protocol with associated types using type erasure
Protocols with associated type, first let’s see what Apple says about them:
When defining a protocol, it’s sometimes useful to declare one or more associated types as part of the protocol’s definition. An associated type gives a placeholder name to a type that is used as part of the protocol. The actual type to use for that associated type isn’t specified until the protocol is adopted. Associated types are specified with the
This seems quite simple and pretty useful, let’s say I want to define protocol that defines identifiable objects.
Let’s write some struct conforming to this protocol to see how hard it can be to use.
Pretty simple and the compiler even infers the actual Identifier type for us, but in the case it cannot do it we can always spell it out for it:
typealias Identifier = ImmatriculationPlate
If we are being totally honest the
Identifiable protocol is not doing much for us. Let’s take a look at an actual use case for protocol with associated type. I was recently working on an application that needed to display a catalog of products, so it was pretty classic: my ViewController fetched the products from a repository and displayed them.
But after a while there were three different kind of product A, B and C. Each kind of product had its own characteristics and so was described in my app by a struct. So let’s make this viewController generic, and for that we need our
CatalogFetcher protocol to have an associated type and in that context for that type to be our generic product type.
Now that we have our solution we just have to implement it: This seems pretty simple
CatalogViewController<Catalog> and everything falls into place, we just have to make
CatalogFetcher a PAT (Protocol with Associated Type) and we are all set.
private let catalogFetcher: CatalogFetcher causes the error:
Protocol 'CatalogFetcher' can only be used as a generic constraint because it has Self or associated type requirements.
This was too good and too simple to be true, let’s dive in and find out what this is all about.
Swift is a strongly typed language which means every variable should have a type and this type has to be completely defined. Therefore
CatalogFetcher has no meaning by itself to the compiler, indeed to make sense of a type the compiler should be able to know what variable and function can be called from that variable. In this case the compiler needs to know the actual type of
In order to have a
CatalogFetcher variable we have to create a type whose sole purpose is to present the
CatalogFetcher API. Let’s see two workarounds: Generic Wrapper and Type Erasure.
The easiest way to create such a type is to create a generic wrapper that only display the wanted API.
This way we can wrap any kind of type.
ImplementingType that implement
CatalogFetcher inside a
CatalogFetcherWrapper that only present
CatalogFetcher API. Unfortunately this imply that our variable has to be:
let catalogFetcher: CatalogFetcherWrapper<ImplementingType> which means for our ViewController to be able to use any implementation of
CatalogFetcher we need to add another generic type parameter to our protocol:
This actually suits our needs but it is not quite the best way to do it: because even though the wrapper does not enable to do anything else than what the API of the
CatalogFetcher provides, it does not feel right that our viewController knows what is the underlying implementing type, and its awful to understand on the first read.
In the wrapper solution the implementing type enables the compiler to infer what kind of catalog we are manipulating. But now let’s try to hide that implementing type ; keep it private and only show the
Catalog type used. This pattern is called type erasure since we erase the implementing type to only display the associated type. In order to avoid mixing thing up here are the definitions of the terms we are going to use:
AssociatedType: the placeholder of the protocol which represent a type associated to the protocol
ImplementingType: the type of the object implementing the protocol
ConcreteType: the type associated to the
In order to achieve type erasing we are going to use 3 layers of boilerplate:
AbstractBase: The abstract base is a generic abstract class that implements protocol and where the associated type of the protocol is its generic parameter type
Private Box: The private box is the same generic wrapper described in the previous section, but the important difference is that it inherits from the
AbstractBasewhich is the reason we can erase the implementing type using the polymorphism of this object.
Public Wrapper: The public wrapper is the class were going to use once all is setup, its job is mainly to present a clean interface and be an opaque wrapper for the
Theses classes and their initialization methods are summed up in the below figure:
In order to make this as clear as possible lets look at the implementation of our boilerplate classes in our concrete example.
Since generic abstract classes does not exist per say in swift we are forced to manufacture one, making init unavailable and providing crashing implementation of the methods that have to be overridden by the subclasses.
The private box take advantage of inheritance and in order to be able to be seen either as a generic class whose type parameter is the implementing type, or one where the type parameter is the concrete type associated to it.
As mentioned before in the wrapper class we only wrap the box and use polymorphism to see it as a
_AnyCatalogFetcherBase which is a generic of the concrete type instead of the implementing type.
These 3 classes can be declared in the same file so that the first 2 class can be declared private so that we only expose the final
AnyCatalogFetcher. Let’s see what our viewController looks like with a type erased wrapper.
We now can enjoy our generic viewController that will be able to handle any type of