Skip to content
AgileLogics
6 min read

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.