Published on
/
3 min read

The Self Conundrum

Authors

Before reading I highly recommend you check out this talk:

If you've dealt with protocols in Swift then it's a rite of passage to see the following error message:

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

This has already been blogged to death and there are plenty of resources explaining how to solve the error, but it still took me a while to fully understand the problem and why the solutions work.

Swift is a very strict, statically-typed language and the compiler won't be happy until it can figure out all possible types by compilation time. Using associatedtype or Self within a protocol is like dropping in a placeholder, or abstract type, that will be satisfied later by a conforming type. Clearly this goes against the compiler's goal of computing each and every type if there are placeholder types yet to be determined.

This is only a problem when trying to program against the base protocol type. You can always use a conforming type because the placeholder has been filled in. For example:

// Error: will complain about self requirement since func ==(lhs: Self, rhs: Self) satisfied
let myProperty: Hashable

// Works fine
let otherProperty: String

Design Considerations

You should be careful and deliberate when introducing a self requirement to a protocol and consider how it will be used. If the protocol type needs to be stored in a property, like the above code, or used as a function parameter then you need to either resolve the placeholder type with generics or type erasure. You may have used AnyHashable before which is Swift's type-erased version of Hashable.

// Now this works
let myProperty: AnyHashable

And with generics:

struct MyStruct<T: Hashable> {
  let myProperty: T
}

Both solutions actually boil down to using generics, since type erasure utilizes a thunk with a generic initializer to create the boxed inner value. Take a look at the AnyHashable source to understand what I mean.

Another common scenario is whether the protocol will be used in a collection type. Same as before you will need to use generics or type erasure, however, the usage of the collection may alter your choice. If you want to have a collection of mixed conforming types, then it's easier to use type erasure, similar to how Dictionary uses [AnyHashable: String] to allow multiple key types like dict[0] = "blah" and dict["blah"] = "blah". Using generics is easier when you have a homogeneous collection.

Conclusion

With all that said, self requirements are an incredibly useful tool that can make your code even more type-safe. Just keep in mind all the extra baggage they bring along and understand the tradeoffs.