Polyglot
Languages Swift generics
Swift § generics

Generics

Swift’s generics admit writing functions and types parameterised by other types. The principal forms: generic functions, generic types (struct, class, enum), generic protocols (with associatedtype), generic constraints (via where and :), opaque types (some Protocol), and existential types (any Protocol). The combination — substantial type-level expressiveness, type inference at the call site, the protocol-with-associated-types form (PATs), the some/any distinction (Swift 5.1+/5.6+) — is the substance of Swift’s generic surface. The conventional Swift discipline favours protocols with associated types over concrete generic types; the protocol-oriented programming style admits substantial flexibility.

Generic functions

Type parameters appear in angle brackets after the function name:

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

var x = 1
var y = 2
swapValues(&x, &y)                                 // T inferred as Int

var s1 = "hello"
var s2 = "world"
swapValues(&s1, &s2)                               // T inferred as String

The inout admits in-place modification; treated in Functions and closures.

For multiple type parameters:

func pair<A, B>(_ a: A, _ b: B) -> (A, B) {
    (a, b)
}

let p = pair(1, "hello")                           // (Int, String)

Generic constraints

Constraints admit “this type must conform to a protocol”:

func max<T: Comparable>(_ a: T, _ b: T) -> T {
    a > b ? a : b
}

max(3, 5)                                          // 5
max("a", "b")                                      // "b"
max(3.0, 5.0)                                      // 5.0

The T: Comparable admits using <, >, <=, >= on T.

For substantial constraints, the where clause:

func sum<T: Sequence>(_ s: T) -> T.Element
where T.Element: Numeric {
    s.reduce(0, +)
}

sum([1, 2, 3])                                     // 6
sum([1.0, 2.0, 3.0])                              // 6.0

The where admits substantial expressiveness — constraints on associated types, multiple-type relationships, etc.

Generic types

Type parameters in struct/class/enum:

struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }

    var top: Element? {
        items.last
    }
}

var s = Stack<Int>()
s.push(1)
s.push(2)
let top = s.pop()                                  // Optional<Int>

var t = Stack<String>()
t.push("hello")

Generic enums:

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

let ok: Result<Int, MyError> = .success(42)
let err: Result<Int, MyError> = .failure(.timeout)

The standard library’s Result<Success, Failure> (introduced in Swift 5.0) follows this pattern.

Type inference for generics

Type arguments are typically inferred from the usage:

func first<T>(_ array: [T]) -> T? {
    array.first
}

let n = first([1, 2, 3])                           // T inferred as Int; returns Int?
let s = first(["a", "b"])                          // T inferred as String

// Explicit type arguments (rarely needed):
let x = first<Int>([1, 2, 3])                      // not admitted in this position
let x: Int? = first([1, 2, 3])                     // disambiguate via return type

Swift admits substantial inference; explicit type arguments are conventional only when inference fails or for clarity.

Generic protocols and associated types

Protocols admit associated types — placeholders that conformers fill in:

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

struct IntStack: Container {
    typealias Item = Int                           // explicit (often optional)
    var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

struct GenericStack<T>: Container {
    var items: [T] = []
    var count: Int { items.count }
    mutating func append(_ item: T) { items.append(item) }
    subscript(i: Int) -> T { items[i] }
    // Item inferred as T
}

The conformance is satisfied when the type provides the associated types and required members. The Item may be inferred from the methods (append, subscript).

Constraints on associated types

protocol Sequence {
    associatedtype Element
    associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
}

protocol Numeric: Comparable, ExpressibleByIntegerLiteral {
    associatedtype Magnitude: Comparable, Numeric
    var magnitude: Magnitude { get }
    static func * (lhs: Self, rhs: Self) -> Self
    // ...
}

The where admits substantial constraints — associated types must satisfy specific protocols and relationships.

Opaque types some

The some keyword (Swift 5.1+) admits an opaque return type — the function returns some specific type conforming to the protocol, but the caller doesn’t see which type:

func makeShape() -> some Shape {                   // returns a specific Shape; caller doesn't know which
    Circle(radius: 5)
}

let s = makeShape()                                // type: some Shape
                                                    // caller can call Shape methods; not access Circle specifics

The mechanism admits substantial type abstraction without exposing the concrete type. Conventional in SwiftUI:

struct ContentView: View {
    var body: some View {                          // some specific View type
        VStack {
            Text("Hello")
            Button("Click me") { /* ... */ }
        }
    }
}

The some View admits the compiler optimising the rendering without exposing the concrete view-tree type.

Existential types any

The any keyword (Swift 5.6+) admits an existential type — a value of any type conforming to the protocol:

let shapes: [any Shape] = [Circle(radius: 5), Square(side: 4), Triangle(...)]

for shape in shapes {
    print(shape.area())                            // calls the conforming type's method
}

The mechanism admits heterogeneous collections of conforming types; each element may be a different concrete type.

The principal differences:

FormDescription
some ProtocolSpecific (but unnamed) conforming type. Static dispatch.
any ProtocolAny conforming type, dynamic. Existential. Dynamic dispatch.

The conventional discipline:

  • Use some when the type is fixed but the caller needn’t know it.
  • Use any when the value may be of different conforming types.
// `some` for "I'm returning some specific Shape":
func makeShape(kind: String) -> some Shape { /* always returns the same type */ }

// `any` for "the parameter may be any Shape":
func process(_ shape: any Shape) { /* ... */ }

// `any` for heterogeneous collection:
let mixed: [any Shape] = [...]

Pre-5.6, Shape (without any) was admitted; the explicit any is the conventional contemporary form.

Generic where clauses

The where admits constraints across the generic parameters:

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

[1, 2, 3].sum()                                    // 6 (Int)
[1.0, 2.0, 3.0].sum()                              // 6.0 (Double)
// ["a", "b"].sum()                                 // ERROR: String is not Numeric

The mechanism admits substantial conditional functionality — methods are admitted only when the constraint is satisfied.

Generic subscripts

struct Container<T> {
    var items: [T]

    subscript<I: Sequence>(indices: I) -> [T] where I.Element == Int {
        indices.map { items[$0] }
    }
}

let c = Container(items: [10, 20, 30, 40, 50])
let selected = c[[0, 2, 4]]                        // [10, 30, 50]

Self requirements

The Self keyword in protocols admits “the conforming type”:

protocol Cloneable {
    func clone() -> Self
}

struct Point: Cloneable {
    let x: Int
    let y: Int

    func clone() -> Self {                         // Self = Point
        Point(x: x, y: y)
    }
}

The mechanism admits substantial fluent APIs — methods that return the conforming type rather than the protocol type.

Common patterns

Generic function with constraint

func findMax<T: Comparable>(in collection: [T]) -> T? {
    collection.max()
}

Generic stack

struct Stack<Element> {
    private var items: [Element] = []

    var isEmpty: Bool { items.isEmpty }
    var count: Int { items.count }
    var top: Element? { items.last }

    mutating func push(_ item: Element) {
        items.append(item)
    }

    @discardableResult
    mutating func pop() -> Element? {
        items.popLast()
    }
}

Generic Result

enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)

    func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure> {
        switch self {
        case .success(let value): return .success(transform(value))
        case .failure(let error): return .failure(error)
        }
    }
}

Protocol with associated types

protocol Repository {
    associatedtype Entity: Identifiable
    associatedtype Query

    func find(_ id: Entity.ID) async throws -> Entity?
    func search(_ query: Query) async throws -> [Entity]
}

struct UserRepository: Repository {
    typealias Entity = User
    typealias Query = String

    func find(_ id: User.ID) async throws -> User? { /* ... */ }
    func search(_ query: String) async throws -> [User] { /* ... */ }
}

Conditional conformance

extension Array: Equatable where Element: Equatable {
    // [Element] is Equatable when Element is Equatable
}

// Now:
let a: [Int] = [1, 2, 3]
let b: [Int] = [1, 2, 3]
a == b                                              // true

The mechanism admits substantial conditional protocol conformance — synthesised in many cases for Equatable, Hashable, Codable.

some for view return

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
            Button("Click") { /* ... */ }
        }
    }
}

any for heterogeneous collection

protocol Animal {
    func sound() -> String
}

struct Dog: Animal { func sound() -> String { "Woof" } }
struct Cat: Animal { func sound() -> String { "Meow" } }
struct Cow: Animal { func sound() -> String { "Moo" } }

let zoo: [any Animal] = [Dog(), Cat(), Cow()]

for animal in zoo {
    print(animal.sound())
}

Generic factory

func make<T: ExpressibleByIntegerLiteral>() -> T {
    return 42                                       // T must admit Int conversion
}

let i: Int = make()
let d: Double = make()

Generic with multiple constraints

func merge<T, U>(_ a: T, _ b: U) -> [String: Any]
where T: Encodable, U: Encodable {
    let aData = try! JSONEncoder().encode(a)
    let bData = try! JSONEncoder().encode(b)
    var aDict = try! JSONSerialization.jsonObject(with: aData) as! [String: Any]
    let bDict = try! JSONSerialization.jsonObject(with: bData) as! [String: Any]
    aDict.merge(bDict) { _, new in new }
    return aDict
}

Generic constraint on associated type

extension Sequence where Element: Hashable {
    func uniqued() -> [Element] {
        var seen = Set<Element>()
        return filter { seen.insert($0).inserted }
    }
}

[1, 1, 2, 3, 3].uniqued()                          // [1, 2, 3]

Type erasure

When some or any doesn’t fit, type erasure admits hiding the concrete type:

struct AnyAnimal: Animal {
    private let _sound: () -> String

    init<A: Animal>(_ animal: A) {
        self._sound = animal.sound
    }

    func sound() -> String {
        _sound()
    }
}

let erased: [AnyAnimal] = [AnyAnimal(Dog()), AnyAnimal(Cat())]

Pre-Swift 5.6, type erasure was the conventional substitute for any; today, any Protocol is conventional.

A note on PATs

Protocols with associated types (PATs) are not directly usable as types pre-Swift 5.7:

protocol Container {
    associatedtype Item
    func first() -> Item?
}

// Pre-5.7:
// let c: Container = ...                          // ERROR: PATs not usable directly

// Solution 1: generic function:
func process<C: Container>(_ container: C) {
    // ...
}

// Solution 2: type erasure (AnyContainer):
struct AnyContainer<T>: Container { /* ... */ }

// Swift 5.7+ via existentials:
let c: any Container = ...

Swift 5.7+ admits PATs via any for substantial flexibility.

A note on the conventional discipline

The contemporary Swift generics advice:

  • Use generics for genuinely generic code.
  • Use T: Protocol constraints where applicable.
  • Use where clauses for substantial constraints.
  • Use protocols with associated types over concrete generic types.
  • Use some for opaque return types (SwiftUI body, etc.).
  • Use any for heterogeneous collections of conforming types.
  • Use type erasure (Any...) when neither some nor any fits.
  • Trust type inference — explicit type arguments are rarely needed.
  • Use conditional conformance (extension X: Y where ...) for substantial conformances.
  • Use Self in protocols for “the conforming type”.

The combination — generic functions, types, protocols with associated types, some/any for type abstraction, conditional conformance, the where clause — is the substance of Swift’s generics. The discipline produces substantial type-safe abstraction with substantial flexibility for protocol-oriented design.