What are primary associated types in Swift 5.7?

Published on: June 8, 2022

Protocols and associated types have always been somewhat of an interesting beast. They were hard to use sometimes, and before Swift 5.1 we would always have to resort to generics. Consider the following example:

class MusicPlayer {
  func play(_ playlist: Collection) { /* ... */ } 
}

This example wouldn’t compile, and it still wouldn’t today. The reason is that Collection has various associated types that must be clear if we want to use Collection.

A common workaround is to use a generic:

class MusicPlayer<Playlist: Collection> {
  func play(_ playlist: Playlist) { /* ... */ } 
}

Instead of using Collection as an existential (a box that holds an object that conforms to Collection) we use Collection as a constraint on a generic type that we called Playlist. This means that the compiler will always know which object is used to fill in Playlist.

In Swift 5.1, the some keyword was introduced which, after some upgrades in Swift 5.7, allows us to write:

class MusicPlayer {
  func play(_ playlist: some Collection) { /* ... */ } 
}

This is nice, but both the generic solution and the some solution have an important issue. We don’t know what’s inside of the Collection. Could be String, could be Track, could be Album, there’s no way to know. This makes func play(_ playlist: some Collection) practically useless for our MusicPlayer.

In Swift 5.7, protocols can specify primary associated types. These associated types are a lot like generics. They allow developers to specify the type for a given associated type as a generic constraint.

For Collection, the Swift library added a primary associated type for the Element associated type.

This means that you can specify the element that must be in a Collection when you pass it to a function like our func play(_ playlist: some Collection). Before I show you how, let’s take a look at how a protocol defines a primary associated type:

public protocol Collection<Element> : Sequence {

  associatedtype Element
  associatedtype Iterator = IndexingIterator<Self>
  associatedtype SubSequence : Collection = Slice<Self> where Self.Element == Self.SubSequence.Element, Self.SubSequence == Self.SubSequence.SubSequence

  // a lot of other stuff
}

Notice how the protocol has multiple associated types but only Element is written between <> on the Collection protocol. That’s because Element is a primary associated type. When working with a collection, we often don’t care what kind of Iterator it makes. We just want to know what’s inside of the Collection!

So to specialize our playlist, we can write the following code:

class MusicPlayer {
  func play(_ playlist: some Collection<Track>) { /* ... */ }
}

Note that this is functionally equivalent to the following if Playlist is only used in one place:

class MusicPlayer<Playlist: Collection<Track>> {
  func play(_ playlist: Playlist) { /* ... */ }
}

Note that this also works with the any keyword. For example, if we want to store our playlist on our MusicPlayer, we could write the following code:

class MusicPlayer {
    var playlist: any Collection<Track> = []

    func play(_ playlist: some Collection<Track>) {
        self.playlist = playlist
    }
}

With primary associated types we can write much more expressive and powerful code, and I’m very happy to see this addition to the Swift language.

Categories

Swift WWDC 2022