Advanced Generics: Associated Types and where Clauses
Prerequisites: Generics, Opaque and existential types
Protocols can be generic too. Instead of <T>, a protocol declares an
associated type — a placeholder each conforming type fills in.
Associated types
protocol Container {
associatedtype Item
var count: Int { get }
mutating func add(_ item: Item)
func item(at index: Int) -> Item
}
struct IntBox: Container {
private var items: [Int] = []
var count: Int { items.count }
mutating func add(_ item: Int) { items.append(item) }
func item(at index: Int) -> Int { items[index] }
}
struct Queue<Element>: Container {
private var items: [Element] = []
var count: Int { items.count }
mutating func add(_ item: Element) { items.append(item) }
func item(at index: Int) -> Element { items[index] }
}
var box = IntBox()
box.add(7)
var queue = Queue<String>()
queue.add("first")
print(box.item(at: 0), queue.item(at: 0))
7 first
Swift infers Item from the methods: Int for IntBox, and
Element for Queue. Standard protocols work this way:
Collection has an associated Element, which is why generic code can
talk about "the element type of any collection".
where clauses
A where clause adds requirements on type parameters and their
associated types:
func allEqual<C: Container>(_ container: C) -> Bool where C.Item: Equatable {
guard container.count > 1 else { return true }
let first = container.item(at: 0)
return (1..<container.count).allSatisfy { container.item(at: $0) == first }
}
func haveSameFirst<A: Collection, B: Collection>(_ a: A, _ b: B) -> Bool
where A.Element == B.Element, A.Element: Equatable {
a.first == b.first
}
var q = Queue<Int>()
q.add(3); q.add(3)
print(allEqual(q))
print(haveSameFirst([1, 2], Set([1])))
true
true
haveSameFirst accepts two different collection types — an array and
a set — as long as their elements have the same, comparable type.
Primary associated types
Protocols can name their main associated type in angle brackets, which
makes constrained some and any types readable:
func sum(_ numbers: some Collection<Int>) -> Int {
numbers.reduce(0, +)
}
print(sum([1, 2, 3]))
print(sum(Set([10, 20])))
6
30
some Collection<Int> means "some specific collection whose elements
are Ints".
When it's worth it
Generic, protocol-based code shines in libraries and in utilities used across many types. In app code, prefer the simplest thing that works — heavy generics can make both error messages and compile times worse.