Combining Swift’s flexible generics system with protocol-oriented programming can often lead to some really powerful implementations, all while minimizing code duplication and enabling us to establish clearly defined levels of abstraction across our code bases. However, when writing that sort of code before Swift 5.7, it’s been very common to run into the following compiler error:
Protocol 'X' can only be used as a generic constraint because it
has Self or associated type requirements.
Let’s take a look at how Swift 5.7 (which is currently in beta as part of Xcode 14) introduces a few key new features that aim to make the above kind error a thing of the past.
Like we took a closer look at in the Q&A article “Why can’t certain protocols, like Equatable and Hashable, be referenced directly?”, the reason why it’s so common to encounter the above compiler error when working with generic protocols is that as soon as a protocol defines an associated type, the compiler starts placing limitations on how that protocol can be referenced.
For example, let’s say that we’re working on an app that deals with various kinds of groups, and to be able to reuse as much of our group handling code as possible, we’ve chosen to define our core Group
type as a generic protocol that lets each implementing type define what kind of Item
values that it contains:
protocol Group {
associatedtype Item
var items: [Item] { get }
var users: [User] { get }
}
Now, because of that associated Item
type, we can’t reference our Group
protocol directly — even within code that has nothing to do with a group’s items
, such as this function that computes what names to display from a given group’s list of users:
func namesOfUsers(addedTo group: Group) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
One way to solve the above problem when using Swift versions lower than 5.7 would be to make our namesOfUsers
function generic, and to then do what the above error message tells us, and only use our Group
protocol as a generic type constraint — like this:
func namesOfUsers<T: Group>(addedTo group: T) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
There’s of course nothing wrong with that technique, but it does make our function declaration quite a bit more complicated compared to when working with non-generic protocols, or any other form of Swift type (including concrete generic types).
Thankfully, this is a problem that Swift 5.7 neatly solves by expanding the some
keyword (that was introduced back in Swift 5.1) to also be applicable to function arguments. So, just like how we can declare that a SwiftUI view returns some View
from its body
property, we can now make our namesOfUsers
function accept some Group
as its input:
func namesOfUsers(addedTo group: some Group) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
Just like when using the some
keyword to define opaque return types (like we do when building SwiftUI views), the compiler will automatically infer what actual concrete type that’s passed to our function at each call site, without requiring us to write any extra code. Neat!
Sometimes, though, we might want to add a few more requirements to a given parameter, rather than just requiring it to conform to a certain protocol. For example, let’s say that we’re now working on an app that lets our users bookmark their favorite articles, and that we’ve created a BookmarksController
with a method that lets us pass an array of articles to bookmark:
class BookmarksController {
...
func bookmarkArticles(_ articles: [Article]) {
...
}
}
However, not all of our call sites might store their articles using an array. The following ArticleSelectionController
, for instance, uses a dictionary to keep track of what articles that have been selected for what IndexPath
within a UITableView
or UICollectionView
. So, when passing that collection of articles to our bookmarkArticles
method, we first need to manually convert it into an array — like this:
class ArticleSelectionController {
var selection = [IndexPath: Article]()
private let bookmarksController: BookmarksController
...
func bookmarkSelection() {
bookmarksController.bookmarkArticles(Array(selection.values))
...
}
}
But if we instead wanted to update that bookmarkArticles
method to work well for any kind of Collection
that contains Article
values, then we couldn’t simply change its parameter type to some Collection
, since that wouldn’t be enough to specify that we’re looking for a collection that has a specific Element
type as input.
We could, however, once again use a set of generic type constraints to solve that problem:
class BookmarksController {
...
func bookmarkArticles<T: Collection>(
_ articles: T
) where T.Element == Article {
...
}
}
Again, nothing wrong with that — but Swift 5.7 once again introduces a much more lightweight way to express the above kind of declaration, which works the exact same way as when specializing a concrete generic type (such as Array<Article>
). That is, we now can simply tell the compiler what Element
type that we’d like our input Collection
to contain by adding that type within angle brackets right after the protocol name:
class BookmarksController {
...
func bookmarkArticles(_ articles: some Collection<Article>) {
...
}
}
Very cool! We can even nest those kinds of declarations — so if we wanted to make our BookmarksController
capable of bookmarking any kind of value that conforms to a generic ContentItem
protocol, then we could specify some ContentItem
as our collection’s expected Element
type, rather than using the concrete Article
type:
protocol ContentItem: Identifiable where ID == UUID {
var title: String { get }
var imageURL: URL { get }
}
class BookmarksController {
...
func bookmark(_ items: some Collection<some ContentItem>) {
...
}
}
The above works thanks to a new Swift feature called primary associated types, and the fact that Swift’s Collection
protocol declares Element
as such an associated type, like this:
protocol Collection<Element>: Sequence {
associatedtype Element
...
}
Of course, being a proper Swift feature, we can also use primary associated types within our own protocols as well, using the exact same kind of syntax.
Finally, let’s take things one step further by also turning our ArticleSelectionController
into a generic type that can be used to select any ContentItem
-conforming value, rather than just articles. As we’re now looking to mix multiple concrete types that all conform to the same protocol, the some
keyword won’t do the trick — since, like we saw earlier, it works by having the compiler infer a single concrete type for each call site, not multiple ones.
This is where the new any
keyword (which was introduced in Swift 5.6) comes in, which enables us to refer to our ContentItem
protocol as an existential. Now, doing that does have certain performance and memory implications, as it effectively works as an automatic form of type erasure, but in situations where we want to be able to dynamically store a heterogeneous collection of elements, it can be incredibly useful.
For example, by simply using any ContentItem
as our selection
dictionary’s value type, we’ll now be able to store any value conforming to that protocol within that dictionary:
class ContentSelectionController {
var selection = [IndexPath: any ContentItem]()
private let bookmarksController: BookmarksController
...
func bookmarkSelection() {
bookmarksController.bookmark(selection.values)
...
}
}
However, making that change does introduce a new compiler error, since our BookmarksController
is expecting to receive a collection that contains values that all have the exact same type — which isn’t the case within our new ContentSelectionController
implementation.
Thankfully, fixing that issue is as simple as replacing some ContentItem
with any ContentItem
within our bookmark
method declaration:
class BookmarksController {
...
func bookmark(_ items: some Collection<any ContentItem>) {
...
}
}
We’re even able to mix any
and some
references, and the compiler will automatically help us translate between the two. For example, if we wanted to introduce a second, single-element bookmark
method overload, which our first one then simply calls, then we could do so like this (even though the first method’s items
collection contains any ContentItem
and the second method accepts some ContentItem
):
class BookmarksController {
...
func bookmark(_ items: some Collection<any ContentItem>) {
for item in items {
bookmark(item)
}
}
func bookmark(_ item: some ContentItem) {
...
}
}
Again, it’s important to emphasize the using any
does introduce type erasure under the hood, even if it’s all done automatically by the compiler — so using static types (which is still the case when using the some
keyword) is definitely the preferred way to go whenever possible.
Swift 5.7 doesn’t just make Swift’s generics system more powerful, it arguably makes it much more accessible as well, as it reduces the need to use generic type constraints and other more advanced generic programming techniques just to be able to refer to certain protocols.
Generics is definitely not the right tool for every single problem, but when it turns out to be, being able to use Swift’s generics system in a much more lightweight way is definitely a big win.
I hope that you found this article useful. If you have any questions, comments, or feedback, then feel free to reach out via either Twitter or email. For more information about these new generics features, I recommend watching the excellent “What’s new in Swift” and “Embrace Swift generics” sessions from this year’s WWDC.
Thanks for reading!