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.
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.
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.
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.
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.
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.