Using the 'some' and 'any' keywords to reference generic protocols in Swift 5.7

  

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.

Opaque parameter types

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:
// Error: Protocol 'Group' can only be used as a generic constraint
// because it has Self or associated type requirements.
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!

Primary associated types

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.

Existentials and the 'any' keyword

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.

Conclusion

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!


via: https://www.swiftbysundell.com/articles/referencing-generic-protocols-with-some-and-any-keywords


Share on Facebook  Share on Facebook


Comments
My Test App

Click To See More Photos

Mobile Apps


More Blogs

Other Headlines


Receive News Updates
  
  Daily Vibe Breaking News
 

Become A Fan
RSS Logo Facebook Logo Twitter Logo Youtube Logo


Sponsors
Download the BV mobile app

Best VPN Service