Protocols With Associated Types
This is one of the few articles with a source. See the bottom of the page for more information.
Fear Mongering Preface
Be warned this is one of the hardest, most frustrating, and rare Swift features (at least when using UI). This is because it flips protocols on its head and forces you to think about types differently. Not to mention it often forces either the use of enums with associated values (another advanced topic) or type erasure (a topic I have yet to understand).
Enums with Associated ValuesAs you will find throughout this article, this a topic even I am still trying to grasp. So, I will do my best to explain everything as well as I can, but there will be some gaps in my knowledge. In fact, there is evidence that this is an unfinished feature, so my knowledge may become outdated soon.
Okay, for those I have not scared yet, the advantage for protocols with associated types or PATs is kind of like generics with classes (just kind of). I like to think of them as abstract classes (in Java) that are generic. If you remember from the discussion of protocol
s, our implementation of sets were limited to single types–we used integers specifically.
Associated Types
Let's fix this by making this parametric. Similar to generics, we need to create a name for our parametrizing type. To do this, we use the keyword associatedType
followed by a type name, which can be constrained (more on this later). Let's call the type of our elements of our set Element
: associatedType Element
.
Then, when we want a type to conform or adopt our Set
implementation, we can explicitly dictate the type with a typealias
or be consistent with our typing.
Generic Constraint
This seems all well and dandy... Why is this so hard?
Let's try storing it in a variable...
Oops, that doesn't compile... Well, actually that makes sense. If we think about this as a generic, we need to specify the type. Let's fix that:
The same error!
In fact, when working with PATs you will see this often:
"error: protocol '<SomeProtocol>' can only be used as a generic constraint because it has Self or associated type requirements"
In short, PATs are not types. You can only use them as constraints! This is a huge problem. Let's revisit the Swift standard library.
This, funnily enough, as Gallagher points out, is actually a list of all the ways you cannot use protocol
s with associated types.
Now you can get around some of this with Opaque typing, but that is a topic for another article, as I have yet to understand them!
So, what does this mean for us? We can't use PATs as types. We can only use them to show conformance. This means instead of using our protocol Set
as a type we need a type that conforms to Set
. If you have not reviewed generics and constraints, this may seem like a great sin and that Swift breaks all of your conceptions surrounding parametric polymorphism, but it doesn't. It's just annoying.
Let's say we want to write a function concatenate
that takes in a bunch of sets and concatenates the result. If we were working with a normal protocol, we could do:
But since, again, we can only use Set
as a tool to show conformance. But, we want to keep this as widely applicable as possible. So, we use generics!
This is basically the same, except for two differences. The less important is that we have given Set
a new name. The more important is that all of our implementations of Set
must be the exact same. This was not the case with normal protocols. This is where things get extra dicey.
If you want multiple implementations in the same list, there are two methods you can use. The first, which I highly suggest is enums with associated values–it's so OCaml and is very nice. The other is type erasure, which I sadly am not familiar with yet, so you will need to look into it yourself. But my vague understanding is that you use a wrapper type to abstract away the associated type and distill it into its barebones features to guarantee only what you need.
Conditional Functions
We can use constraints to add functionality! We can add constraints directly to the associated type if it is part of the invariant or conditionally to certain functions.
See constraints for more:
Constraintscontains
seems like something we can implement broadly. Assuming we have access to equality, we can just iterate through the values and check!
Or, better yet, we can just Array's implementation of contains!
For those of you that remember default implementations, this allows us to create conditional default implementations! This means that if we ever implement Set
with a type that conforms to Equatable
, we don't need to implement contains! Otherwise, we do.
Credits
Most of my understanding as well as my discussion originates from the following video. It is somewhat outdated, but it is a nice narrative of why this feature is the way it is and how to use it (kind of):
Last updated