Swift Protocols With Associated Types and Generics
Advanced protocols
Today, we’re going to look at the infamous Protocol can only be used as a generic constraint because it has Self or associated type requirements
error message.
Swift documentation describes protocols as “a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.” Using protocols to define responsibilities of different parts of our code makes it easier to read and helps maintain a clean architecture.
Even though Swift is great for object-oriented programming, it was designed as a protocol-oriented language. Or as Apple’s Dave Abrahams put it at 2015 WWDC, “We have a saying in Swift. Don’t start with a class. Start with a protocol.”
Protocols in Action
Let’s see how protocols work by creating some basic logic for a simple online wallet app. First, we’ll definitely need to fetch some data from the server.
After we fetch the data, we’ll parse it to some object. Since we want to keep our options open, it’s a good idea to use an associated type.
We started with protocols. Now, let’s provide concrete implementations. Since we’re building an online wallet app, we’ll need a User
, a Wallet
, and their corresponding fetchers and parsers.
The only thing left to implement is a consumer of this logic.
The Problem
As you can see, the UserFetcher
and the WalletFetcher
implementations are very similar, so it makes sense to refactor the code and use generics.
Declaring our dataParser
the way we did results in an error: Protocol 'DataParsing' can only be used as a generic constraint because it has Self or associated type requirements
.
Trying to fix our error by specializing the dataParser
gets us another error: Cannot specialize non-generic type 'DataParsing'
.
This problem arises because the compiler needs to know the concrete types at compile time. Most Swift developers encounter it at some point of their career, and if you’re still reading this article, there’s a decent chance you are among them.
The Solution
A common way of solving this issue is to use type erasure. As the name suggests, we erase the type information by providing a wrapper with concrete implementation.
We can now use our GenericDataFetcher
and remove duplicate code from UserFetcher
and WalletFetcher
.
The only thing that needs to be updated is the DataManager
code.
Final Thoughts
The solution I described is used in several places in the Swift standard library. However, it relies on closures which are allocated on the heap. Therefore, I would suggest you use it sparingly. Because it also adds to code complexity, I try to avoid it and prefer refactoring the code in a way that eliminates the need for this kind of workaround. The example we used to demonstrate the problem could definitely be refactored differently, but that’s a topic for another piece.
Thanks for reading and please let me know if you have any comments or questions!