Skip to content
Generics with Constraints
step 1/7

Reading — step 1 of 7

Learn

~3 min readGenerics, Concurrency, and Memory

Swift's generics let you write code that works with any type — but with constraints that ensure the type has the capabilities you need. Apple's Swift Programming Language book covers generics across two chapters; this lesson focuses on constraints and where clauses, which separate competent from advanced Swift code.

Basic generics (review)

func swap<T>(_ a: inout T, _ b: inout T) {
    let tmp = a
    a = b
    b = tmp
}

var x = 1, y = 2
swap(&x, &y)

T is a type parameter. The compiler instantiates the function for each type used at the call site (monomorphization).

Type constraints with where

Constrain T to types that conform to a protocol:

func largest<T: Comparable>(_ items: [T]) -> T? {
    guard let first = items.first else { return nil }
    return items.dropFirst().reduce(first) { $0 > $1 ? $0 : $1 }
}

T: Comparable means "T must conform to Comparable" — gives you <, >, etc.

Multiple constraints with & and where:

func allEqual<S: Sequence>(_ seq: S) -> Bool
where S.Element: Equatable {
    var iter = seq.makeIterator()
    guard let first = iter.next() else { return true }
    while let next = iter.next() {
        if next != first { return false }
    }
    return true
}

allEqual([1, 1, 1])         // true
allEqual([1, 2, 1])         // false

The where S.Element: Equatable constrains the associated type of S — Sequences whose elements support ==.

Protocols with associated types

A protocol can declare placeholder types that conformers fill in:

protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct Stack<Element>: Container {
    typealias Item = Element
    private var items: [Element] = []
    var count: Int { items.count }
    mutating func append(_ item: Element) { items.append(item) }
    subscript(i: Int) -> Element { items[i] }
}

The Item is associated — each conformer picks one. Compile-time bound.

Generic extensions with where

extension Array where Element: Numeric {
    func sum() -> Element {
        return reduce(0, +)
    }
}

[1, 2, 3].sum()        // 6 — only works on numeric arrays
["a", "b"].sum()       // ERROR — String isn't Numeric

This lets you add methods to a generic type ONLY when its parameter satisfies a constraint. Powerful for type-specific APIs without subclassing.

Conditional conformance

struct Pair<A, B> {
    let first: A
    let second: B
}

extension Pair: Equatable where A: Equatable, B: Equatable {
    static func == (lhs: Pair, rhs: Pair) -> Bool {
        return lhs.first == rhs.first && lhs.second == rhs.second
    }
}

Pair is Equatable IF both its components are Equatable. Same idea drives Array: Equatable where Element: Equatable in the standard library.

Same-type constraints

The most powerful where clause — equate two associated types:

func combineSequences<S1: Sequence, S2: Sequence>(
    _ a: S1, _ b: S2
) -> [S1.Element]
where S1.Element == S2.Element {
    return Array(a) + Array(b)
}

Forces both sequences to have the same element type. Without this, you couldn't combine them safely.

Common mistakes

  • Constraining with class hierarchies when a protocol would do — protocols are the Swift way.
  • Not using where for clarity — long inline constraints can make signatures unreadable. Move to where clauses.
  • Forgetting associatedtype for protocols with element types — the protocol becomes useless without it.
  • Trying to use == between two generic types without constraint — the compiler doesn't know they're Equatable. Add T: Equatable.
  • Excessive constraints — sometimes only one method needs the constraint. Use a where clause on the specific method, not the whole type.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…