Home

Generics

Constraints, specialisation, and what each one costs

Generics let you write an algorithm once and keep full type information. Unlike Java’s erasure or C macros, Swift generics are type-checked once at the definition and can then be specialised per concrete type at the call site — you get one implementation to maintain and, often, machine code as tight as if you had written it by hand.

func firstIndex<C: Collection>(of element: C.Element, in collection: C) -> C.Index?
    where C.Element: Equatable
{
    var index = collection.startIndex
    while index != collection.endIndex {
        if collection[index] == element { return index }
        collection.formIndex(after: &index)
    }
    return nil
}

Read the constraints as the interface. C: Collection says what operations exist; the where clause says elements can be compared. Anything not stated is not available — which is the point, because it is also what lets the compiler check the body once instead of once per caller.

Constraints belong in where clauses

Two ways to say almost the same thing:

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

extension Collection where Element: Numeric {
    func total() -> Element { reduce(0, +) }
}

The second version works on Array, Set, ArraySlice, Range, String.UTF8View and every other collection anyone writes. Constrain to the least specific protocol that supports what the body actually does. It costs nothing and multiplies the reach of the code.

Conditional conformance

A generic type can conform to a protocol only when its parameter does:

struct Pair<T> {
    let first: T
    let second: T
}

extension Pair: Equatable where T: Equatable {}
extension Pair: Hashable where T: Hashable {}
extension Pair: Codable where T: Codable {}

Pair<Int> is now Hashable and can be a dictionary key; Pair<(Int) -> Void> is not, and the compiler will say so at the point of misuse rather than failing deep inside a hash table. This is how Array itself works — [Int] is Equatable, [() -> Void] is not.

Opaque return types

Returning some Collection hides the concrete type while keeping it static:

func evens(upTo limit: Int) -> some Collection<Int> {
    stride(from: 0, to: limit, by: 2).lazy.map { $0 }
}

The caller gets full performance and can use every Collection method, but cannot depend on the return being a LazyMapSequence<StrideTo<Int>, Int> — so you can change the implementation without breaking anyone. That is exactly what any Collection<Int> cannot promise, and exactly what a concrete return type cannot hide.

Tip

A useful test: if the caller would break when you swap map for compactMap internally, the return type is too concrete. If a hot loop shows a dynamic call you did not expect, it is too existential.

Parameter packs

Before Swift 5.9, “a function taking any number of differently-typed arguments” meant writing the same overload seven times — which is why the standard library historically had zip for two sequences and nothing for three. Parameter packs fix that:

struct Tuple<each T> {
    let values: (repeat each T)

    init(_ values: repeat each T) {
        self.values = (repeat each values)
    }
}

func allEqual<each T: Equatable>(_ left: repeat each T, _ right: repeat each T) -> Bool {
    for (l, r) in repeat (each left, each right) where l != r { return false }
    return true
}

each T is a pack of types; repeat expands over it. You will not write these often, but you will read them in library code, and they explain why some standard-library signatures suddenly grew repeat each.

The cost of specialisation

Specialisation is why generic Swift is fast, and it has a price: the compiler emits a separate copy of the function body for every concrete type it specialises for. A heavily generic module can grow binary size noticeably and slow builds down. When a generic function is large, rarely hot, and used with many types, forcing it through an existential deliberately — any Shape instead of some Shape — trades a little speed for a lot of code size. Measure before you assume which one you need.