Modelling with types · core

Protocol extensions and generics

Where protocols gain real behaviour, plus the generic machinery that lets one function work with many types at no runtime cost.

13 min read16 min practice0/3 exercises4 recall cards

By the end you will be able to

  • Give protocols default implementations and explain when they are used
  • Write generic functions and types with constraints
  • Choose between a generic parameter and an existential
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

Collection has dozens of methods — map, filter, first, dropFirst, enumerated — yet a type only needs to supply startIndex, endIndex, index(after:) and a subscript to get all of them. How?

Default implementations

protocol Greeter {
    var name: String { get }
    func greet() -> String
}

extension Greeter {
    func greet() -> String {
        "Hello, \(name)!"
    }
}

struct Person: Greeter {
    var name: String
}

Person never implements greet() and gets it anyway. A conforming type can override the default by providing its own.

Swift

Notice shout() — defined only in the extension, in terms of greet(). Each type gets it without writing anything, and it correctly picks up the overridden greet for the pirate.

Constrained extensions

An extension can apply only where a condition holds:

extension Collection where Element: Numeric {
    var total: Element {
        reduce(0, +)
    }
}

[1, 2, 3].total          // 6
["a", "b"].total         // error: no such member

This is how the standard library gives [Int] a sum-like capability without polluting [String]. Constrained extensions are one of Swift's sharpest tools: behaviour appears exactly where it makes sense and nowhere else.

Generics: one implementation, many types

A generic function keeps the caller's concrete type instead of erasing it:

func firstOrNil<T>(_ items: [T]) -> T? {
    items.isEmpty ? nil : items[0]
}

firstOrNil([1, 2, 3])       // Int?
firstOrNil(["a"])           // String?

T is a placeholder filled in at each call site. Two things are guaranteed: the concrete type is preserved, and there is no existential box. On top of that, when the optimiser can see the function's body (same module with optimisation on, or @inlinable), it generates specialised code per concrete type and can inline the calls — without that visibility, generic code still routes protocol requirements through a small runtime lookup table. Specialisation is an optimisation you usually get, not a language guarantee.

Constraints

An unconstrained T can do almost nothing — the compiler knows nothing about it. Constraints add capability:

func largest<T: Comparable>(_ items: [T]) -> T? {
    items.max()
}

func describe<T: CustomStringConvertible>(_ value: T) -> String {
    "value: \(value.description)"
}

where clauses handle more elaborate conditions:

func merge<C: Collection>(_ collection: C) -> String where C.Element == String {
    collection.joined(separator: ", ")
}
Swift

Generic types

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

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

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

    var count: Int { items.count }
    var isEmpty: Bool { items.isEmpty }
}

var numbers = Stack<Int>()
var words = Stack<String>()

Array, Dictionary, Set and Optional are all generic types written exactly this way.

Swift

Generic or existential?

Both let one function work with several types. They are not interchangeable.

Generic — type preserved
func process<T: Shape>(_ shape: T) {
    // T is one concrete type here.
    // Compiler specialises, inlines,
    // no boxing.
}

// Cannot hold mixed types:
// let mixed: [T] — nonsense
Existential — type erased
func process(_ shape: any Shape) {
    // Concrete type unknown.
    // Dynamic dispatch, possible boxing.
}

// Can hold mixed types:
let mixed: [any Shape] = [
    Circle(), Square()
]

The rule: generic by default. Reach for any only when you genuinely need a heterogeneous collection or must store the value without knowing its type. A generic keeps performance and type information; an existential trades both for flexibility you often do not need.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

A generic constrained to Numeric behaves differently for Int and Double. What does this print?

func total<T: Numeric>(_ values: [T]) -> T {
    values.reduce(0, +)
}

print(total([1, 2, 3]))
print(total([1.5, 2.5]))
print(total([Int]()))

Associated types

A protocol can have a placeholder type of its own:

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

struct IntStack: Container {
    var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(index: Int) -> Int { items[index] }
}

Item is inferred from the implementation — here, Int. This is how Collection declares Element and Iterator.

Associated types are also why some protocols cannot be used as plain existentials without care: any Container does not say what it contains. Swift 5.7 made this workable with primary associated types (any Container<Int>), but generics remain the cleaner answer.

Check yourself

When should you prefer func handle<T: Shape>(_ s: T) over func handle(_ s: any Shape)?

Default implementation

Write the code

Give Identifiable2 a default shortID that returns the first four characters of id, and let one type override it.

Solution unlocks after 3 attempts

Write a generic function

Write the code

Write pairs(_:_:) that zips two arrays into an array of tuples, stopping at the shorter one — for any two element types.

Solution unlocks after 3 attempts

Constrain an extension

Write the code

Add a total property to arrays of Int only — ["a"] must not gain it.

Solution unlocks after 3 attempts
Teach itSaved on this device. Never graded.

Explain to someone who knows Java interfaces what a Swift protocol extension adds that a Java interface's default methods do not. Focus on constrained extensions and on retroactive conformance.

Checkpoint

You can now:

  • Supply default implementations in protocol extensions and know when they apply
  • Constrain an extension so behaviour appears only where it makes sense
  • Write generic functions and types with constraints
  • Choose a generic over an existential, and say why

Next up: the protocols Swift synthesises for you — Equatable, Hashable, Comparable and Codable.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.