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
associatedtype
keyword.
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
Concrete example
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.
Generic implementation
Now that we have our solution we just have to implement it: This seems pretty simple CatalogViewController
becomes CatalogViewController<Catalog>
and everything falls into place, we just have to make CatalogFetcher
a PAT (Protocol with Associated Type) and we are all set.
But now 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 CatalogFetcher.Catalog
.
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.
Generic wrapper
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.
Type erasure
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 protocolImplementingType
: the type of the object implementing the protocolConcreteType
: the type associated to theImplementingType
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 typePrivate Box
: The private box is the same generic wrapper described in the previous section, but the important difference is that it inherits from theAbstractBase
which 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 thePrivateBox
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.
Abstract Base
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.
Private Box
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.
Public wrapper
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 Catalog
.