The different categories of Swift protocols
Basics article available: ProtocolsIn general, the primarily role of protocols (or interfaces) is to enable generic abstractions to be defined on top of concrete implementations — a technique which is commonly referred to as polymorphism, as it enables us to swap (or morph) our implementations without affecting their public API.
While Swift offers full support for that kind of interface-based polymorphism, protocols also play a much larger role in the overall design of the language and its standard library — as a major part of the functionality that Swift ships with is actually implemented directly on top of various protocols.
That protocol-oriented design also enables us to use protocols in many different ways within our own code as well — all of which can essentially be divided into four main categories. This week, let’s go through those categories, and both take a look at how Apple uses protocols within their frameworks, and how we can define our own protocols in a very similar fashion.
Enabling unified actions
Let’s start by taking a look at protocols that require the types that conform to them to be able to perform certain actions. For example, the standard library’s Equatable
protocol is used to mark that a type can perform an equality check between two instances, while the Hashable
protocol is adopted by types that can be hashed:
protocol Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool
}
protocol Hashable: Equatable {
func hash(into hasher: inout Hasher)
}
A big benefit of the fact that those two capabilities are defined using the type system (rather than being hard-coded into the compiler) is that it lets us write generic code that’s constrained to those protocols, which in turn enables us to make full use of those capabilities within such code.
For example, here’s how we could extend Array
with a method that lets us count all occurrences of a value, given that the array’s Element
type conforms to Equatable
:
extension Array where Element: Equatable {
func numberOfOccurences(of value: Element) -> Int {
reduce(into: 0) { count, element in
// We can check whether two values are equal here
// since we have a guarantee that they both conform
// to the Equatable protocol:
if element == value {
count += 1
}
}
}
}
In general, whenever we define action-based protocols, it’s usually a good idea to make those protocols as generic as possible (just like Equatable
and Hashable
), since that lets them remain focused on the actions themselves, rather than being too tied to any specific domain.
So for example, if we wanted to unify several types that load various objects or values, we could define a Loadable
protocol with an associated type — which would let each conforming type declare what sort of Result
that it loads:
protocol Loadable {
associatedtype Result
func load() throws -> Result
}
However, not every protocol defines actions (after all, this is just the first category out of four). For example, while the name of the following Cachable
protocol might suggest that it contains actions for caching, it’s actually just used to enable various types to define their own caching keys:
protocol Cachable: Codable {
var cacheKey: String { get }
}
Compare the above to the built-in Codable
protocol that Cachable
inherits from, which does define actions for both encoding and decoding — and it starts to become clear that we’ve ended up with a bit of a naming mismatch.
After all, not all protocols need to use the able suffix. In fact, forcing that suffix onto any given noun just to define a protocol for it can lead to quite a lot of confusion — like in this case:
protocol Titleable {
var title: String { get }
}
What does “Titleable” even mean?
What’s perhaps even more confusing is when using the ”able” suffix results in a name with a completely different meaning than what we intended. For example, here we’ve defined a protocol with the intention of having it act as an API for color containers, but its name suggests that it’s for types that themselves can be colored:
protocol Colorable {
var foregroundColor: UIColor { get }
var backgroundColor: UIColor { get }
}
So how could we improve some of the these protocols — both in terms of their naming, as well as how they’re structured? Let’s start by stepping out of category number one, and explore a few different ways of defining protocols in Swift.
Defining requirements
Category number two is for protocols that are used to define formal requirements for a given kind of object or API. Within the standard library, such protocols are used to define what it means to be things like a Collection
, a Numeric
, or a Sequence
:
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
Note that the above protocol is not called Sequencable
, since that would indicate that it’s about turning objects into sequences, rather than defining the requirements for being one.
What the above definition of Sequence
tells us is that the primary role of any Swift sequence (such as an Array
, a Dictionary
, or something like a Range
) is to act as a factory for creating iterators — which in turn are formalized through the following protocol:
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
The above protocol could arguably have been called Iterable
instead, since iterators do actually perform each iteration action themselves. However, the name IteratorProtocol
was likely picked to make it feel more consistent with Sequence
, since simply naming it Iterator
would’ve caused conflicts with the associated type of the same name.
With the above two protocols in mind, let’s now go back to the Cachable
and Colorable
protocols that we defined earlier, to see if they can be improved by transforming them into requirement definitions instead.
Let’s start by renaming Colorable
into ColorProvider
, which gives that protocol a whole new meaning — even if its requirements remain exactly the same. It no longer sounds like it’s used to define objects that can be colored, but rather that it’s about providing color information to some other part of our system — which is exactly what we intended:
protocol ColorProvider {
var foregroundColor: UIColor { get }
var backgroundColor: UIColor { get }
}
Similarly, taking inspiration from the built-in IteratorProtocol
, we could rename Cachable
into something like this:
protocol CachingProtocol: Codable {
var cacheKey: String { get }
}
However, an arguably even better approach in this case would be to decouple the concept of generating caching keys from the types that are actually being cached — which would let us keep our model code free from caching-specific properties.
One way to do that would be to move our key generation code into separate types — which we could then formalize the requirements for using a CacheKeyGenerator
protocol:
protocol CacheKeyGenerator {
associatedtype Value: Codable
func cacheKey(for value: Value) -> String
}
Another option would be to model the above as a closure instead, which is often a great alternative to protocols that just contain a single requirement.
Type conversions
Next, let’s take a look at protocols that are used to declare that a type is convertible to and from other values. We’ll again start with an example from the standard library — CustomStringConvertible
, which can be used to enable any type to be converted into a custom description string:
protocol CustomStringConvertible {
var description: String { get }
}
Compare the above to what it could’ve looked like if it was called Describable
instead. If so, the expectation would probably have been that it contains a describe()
method, or something similar.
That kind of design is particularly useful when we want to be able to extract a single piece of data from multiple types — which perfectly matches the purpose of our (somewhat strangely named) Titleable
protocol from earlier.
By renaming that protocol to TitleConvertible
instead, we not only make it easier to understand what that protocol is for, we also make our code more consistent with the standard library — which is most often a good thing:
protocol TitleConvertible {
var title: String { get }
}
Type conversion protocols can also use methods, rather than properties, which is typically a better fit when we expect certain implementations to require a fair amount of computation — for example when working with image conversions:
protocol ImageConvertible {
// Since rendering an image can be a somewhat expensive
// operation (depending on the type being rendered), we're
// defining our protocol requirement as a method, rather
// than as a property:
func makeImage() -> UIImage
}
We can also use this category of protocols to enable certain types to be expressed in different ways — a technique which is, among other things, used to implement all of Swift’s built-in support for literals — such as string and array literals. Even nil
assignments are implemented through a protocol, which is quite cool:
protocol ExpressibleByArrayLiteral {
associatedtype ArrayLiteralElement
init(arrayLiteral elements: ArrayLiteralElement...)
}
protocol ExpressibleByNilLiteral {
init(nilLiteral: ())
}
Note that while we’re free to conform to most built-in literal protocols within our own code as well, conforming to ExpressibleByNilLiteral
is discouraged — as Optional
is expected to be the only type adopting that protocol.
While it’s perhaps not that common to define our own protocols for bridging literals into an instances of a type (since that does, in fact, require changes to the compiler), we can use that same design whenever we want to declare a protocol for expressing a type using a lower-level representation.
For example, here’s how we could define an ExpressibleByUUID
protocol for identifier types that can be created using a raw UUID
:
protocol ExpressibleByUUID {
init(uuid: UUID)
}
Another option would be to use the RawRepresentable
protocol, which is what powers enums that have raw values. However, while that protocol is definitely also a type converting one, its initializer is failable — which means that it’s really only useful for conditional conversions that could potentially result in nil
.
Abstract interfaces
Finally, let’s take a look at perhaps the most common way of using protocols within third party code — to define abstractions for interfacing with multiple underlying types.
An interesting example of this pattern can be found within Apple’s Metal framework, which is a low-level graphics programming API. Since GPUs tend to vary a lot between devices, and Metal aims to provide a unified API for programming against any type of hardware that it supports, it uses a protocol to define its API as an abstract interface — which looks like this:
protocol MTLDevice: NSObjectProtocol {
var name: String { get }
var registryID: UInt64 { get }
...
}
When using Metal, we can then call the MTLCreateSystemDefaultDevice
function, and the system will return an implementation of the above protocol that’s appropriate for the device that our program is currently running on:
func MTLCreateSystemDefaultDevice() -> MTLDevice?
Within our own code, we can also use that exact same pattern whenever we want to support multiple implementations of the same interface. For example, we might define a NetworkEngine
protocol in order to decouple the way we perform our network calls from any specific means of networking:
protocol NetworkEngine {
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
)
}
With the above in place, we’re now free to define as many underlying networking implementations as we need — for example a URLSession
-based one for production, and a mocked version for testing:
extension URLSession: NetworkEngine {
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
) {
...
}
}
struct MockNetworkEngine: NetworkEngine {
var result: Result<Data, Error>
func perform(
_ request: NetworkRequest,
then handler: @escaping (Result<Data, Error>) -> Void
) {
handler(result)
}
}
To learn more about mocking within unit tests, check out “Mocking in Swift”.
The above technique can also be a great way to encapsulate third party dependencies in order to prevent them from spreading across our entire code base — which in turn makes it much easier to replace or remove those dependencies in the future.
Conclusion
Swift’s implementation of protocols is definitely one of the most interesting aspects of the language, and the sheer number of ways that they can be defined and used really shows just how powerful they are — especially once we start making full use of features like associated types and protocol extensions.
Because of that, it’s important to not treat every protocol the same way, but to rather design them according to which category that they belong to. To recap, these are the four categories that I like to split protocols up into:
- Action enablers, which enable us to perform a given set of actions on each conforming type. They typically have names that end with “able”, such as
Equatable
. - Requirement definitions enable us to formalize the requirements for being a certain kind of object, for example a
Sequence
, aNumeric
, or aColorProvider
. - Type conversion protocols are used to let various types declare that they can be convertible into another type, or expressible through a raw value or literal — like
CustomStringConvertible
orExpressibleByStringLiteral
. - Abstract interfaces act as unified APIs that multiple types can implement, which in turn lets us swap out implementations as we wish, encapsulate third party code, or mock certain objects within our tests.
What do you think? Do you agree with my way of splitting Swift’s various protocols up into those four categories, or do you organize your protocols some other way? Let me know — along with your questions, comments and feedback — either via Twitter or email.
Thanks for reading! 🚀