WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Lightweight same-type requirements for primary associated types

Available from Swift 5.7

Paul Hudson      @twostraws

SE-0346 adds newer, simpler syntax for referring to protocols that have specific associated types.

As an example, if we were writing code to cache different kinds of data in different kinds of ways, we might start like this:

protocol Cache<Content> {
    associatedtype Content

    var items: [Content] { get set }

    init(items: [Content])
    mutating func add(item: Content)
}

Notice that the protocol now looks like both a protocol and a generic type – it has an associated type declaring some kind of hole that conforming types must fill, but also lists that type in angle brackets: Cache<Content>.

The part in angle brackets is what Swift calls its primary associated type, and it’s important to understand that not all associated types should be declared up there. Instead, you should list only the ones that calling code normally cares about specifically, e.g. the types of dictionary keys and values or the identifier type in the Identifiable protocol. In our case we’ve said that our cache’s content – strings, images, users, etc – is its primary associated type.

At this point, we can go ahead and use our protocol as before – we might create some kind of data we want to cache, and then create a concrete cache type conforming to the protocol, like this:

struct File {
    let name: String
}

struct LocalFileCache: Cache {
    var items = [File]()

    mutating func add(item: File) {
        items.append(item)
    }
}

Now for the clever part: when it comes to creating a cache, we can obviously create a specific one directly, like this:

func loadDefaultCache() -> LocalFileCache {
    LocalFileCache(items: [])
}

But very often we want to hide the specifics of what we’re doing, like this:

func loadDefaultCacheOld() -> some Cache {
    LocalFileCache(items: [])
}

Using some Cache gives us the flexibility of changing our mind about what specific cache is sent back, but what SE-0346 lets us do is provide a middle ground between being absolutely specific with a concrete type, and being rather vague with an opaque return type. So, we can specialize the protocol, like this:

func loadDefaultCacheNew() -> some Cache<File> {
    LocalFileCache(items: [])
}

So, we’re still retaining the ability to move to a different Cache-conforming type in the future, but we’ve made it clear that whatever is chosen here will store files internally.

This smarter syntax extends to other places too, including things like extensions:

extension Cache<File> {
    func clean() {
        print("Deleting all cached files…")
    }
}

And generic constraints:

func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C {
    print("Copying all files into a new location…")
    // now send back a new cache with items from both other caches
    return C(items: lhs.items + rhs.items)
}

But what will prove most helpful of all is that SE-0358 brings these primary associated types to Swift’s standard library too, so Sequence, Collection, and more will benefit – we can write Sequence<String> to write code that is agnostic of whatever exact sequence type is being used.

Hacking with Swift is sponsored by RevenueCat

SPONSORED You know StoreKit, but you don’t want to do StoreKit. RevenueCat makes it easy to deploy, manage, and analyze in-app subscriptions on iOS and Android so you can focus on building your app.

Explore the docs

Sponsor Hacking with Swift and reach the world's largest Swift community!

Other changes in Swift 5.7…

Download all Swift 5.7 changes as a playground Link to Swift 5.7 changes

Browse changes in all Swift versions

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.