Articles, podcasts and news about Swift development, by John Sundell.

Why can’t certain protocols, like Equatable and Hashable, be referenced directly?

Answered on 14 Jul 2020
Basics article available: Protocols

If you’ve been doing Swift development for a while, chances are quite high that you’ve encountered a version of the following compiler error at some point:

Protocol 'Equatable' can only be used as a generic constraint because it has
Self or associated type requirements.

That’s something that happens when we try to reference a generic protocol directly — that is, a protocol that has either associated types, or requires the conforming type (which Self refers to) to be known. For example, the built-in Equatable protocol uses Self within its declaration, like this:

protocol Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool
}

That’s because an equality check can only be performed between two values that are known to be of the exact same type — so something like the following function wouldn’t work, since the two arguments passed to it could be of any type conforming to Equatable:

func valueDidChange(from old: Equatable, to new: Equatable) {
    guard old != new else {
        return
    }
    
    ...
}

The following, on the other hand, does work — since we’re now using Equatable as a constraint for the generic type T, which gives us a compile-time guarantee that both arguments will be of the exact same type:

func valueDidChange<T: Equatable>(from old: T, to new: T) {
    guard old != new else {
        return
    }
    
    ...
}

So when Self is being used within a protocol, the compiler needs to know exactly what type that we’re referring to within each given context. To make that happen, we can either use a conforming concrete type directly (such as using Int or String, instead of Equatable), or use the protocol as a generic constraint (like above).

The same thing is also true for protocols that have associated types, since just like Self, the actual types that are being referred to won’t be known unless we provide some additional context. For example, here we’ve defined a Loadable protocol that enables us to load an associated Target type through a load() method:

protocol Loadable {
    associatedtype Target
    func load() -> Target
}

What exact type that Target will refer to will vary depending on what conforming type that we’re calling load() on, so something like the following wouldn’t work — since the compiler can’t tell what type that’s being loaded:

func loadTarget(from loadable: Loadable) {
    let target = loadable.load()
    ...
}

Just like before, we can solve the above problem by using a generic type constraint to give the compiler awareness of what exact Loadable implementation that each call site is using:

func loadTarget<T: Loadable>(from loadable: T) {
    let target = loadable.load()
    ...
}

Now, whenever we’re calling the above loadTarget function, the compiler will be able to infer what type that T refers to based on the Loadable type that’s being passed into our function. For example, here the compiler can infer that T means StringLoader and that the Target being loaded is a String value:

struct StringLoader: Loadable {
    func load() -> String {
        ...
    }
}

let loader = StringLoader()
loadTarget(from: loader)

There are also a number of other techniques that can be really useful when dealing with generic protocols — including type erasure, using more advanced constraints, and more. Feel free to explore the category pages for generics and protocols to learn more.