Skip to content
Theory · Protocols and generics · lesson 8 of 9

Generic functions and types

Say you need a function that finds the largest number in an array. Then you need the "largest" string, that is, the last one in alphabetical order. The code of the two functions matches letter for letter, and only the type differs: [Int] in one and [String] in the other. Copying the function for every type is inconvenient: a fix in one copy has to be repeated in all the others.

Picture a machine with an adjustable slot. Before work, the slot is fitted to the part, and the machine processes a bolt, a nut or a washer with the very same movements. A plate on the machine states a requirement for the part, for example "metal only". A part that does not meet the requirement will not fit into the slot.

A function or a type with such a slot is called generic: the type of its values is not written in advance but filled in when it is used.

A generic function

Let's write one function for numbers, strings and any other values that can be compared:

SWIFT
func largest<T: Comparable>(_ items: [T]) -> T? {
    if items.isEmpty {
        return nil
    }
    var best = items[0]
    for item in items {
        if item > best {
            best = item
        }
    }
    return best
}

print(largest([3, 9, 4]) ?? 0) // 9
print(largest(["pear", "apple"]) ?? "none") // pear

Let's take the declaration apart:

  1. <T: Comparable> after the function's name is the slot. T is a type parameter: a name that a real type takes the place of in each call.
  2. : Comparable is the plate on the machine, a constraint: only a type that conforms to Comparable fits the slot.
  3. _ items: [T] and -> T? mean an array of values of that type goes in, and one such value comes out, or nil if the array is empty.
  4. In the call largest([3, 9, 4]) Swift sees an array of Int and puts Int in place of T. For ["pear", "apple"] it puts in String.

The result's type is not lost: largest([3, 9, 4]) returns an Int?, and ?? 0 supplies a number.

Here the machine picture is inaccurate. A machine is refitted by hand, while nobody sets up a generic function: Swift picks T from the arguments of each call itself.

Why a constraint is needed

Inside largest there is a comparison, item > best. Not every type has the > operator, only Comparable ones, and the constraint promises the compiler that T will have it. The compiler enforces that promise from both sides:

  • the function's body may use only what the constraint provides;
  • a call with a type that is not Comparable does not compile.

If you pass largest an array of Point structs that are not Comparable, the compiler stops the call:

TEXT
error: global function 'largest' requires that 'Point' conform to 'Comparable'

A generic type

Types can have a slot too. A stack of plates works the same way whatever plates are in it: a plate goes on top and comes off the top.

SWIFT
struct Stack<Element> {
    var items: [Element] = []
    var top: Element? { items.last }
    mutating func push(_ item: Element) {
        items.append(item)
    }
}

var plates = Stack<String>()
plates.push("blue")
plates.push("red")
print(plates.top ?? "empty", plates.items.count) // red 2

What matters here:

  • Element is a type parameter, like T. You may choose any name, as long as it reads well.
  • Stack<String>() gives the type in angle brackets, and push accepts only strings.
  • There is no constraint here: the stack does not need to compare plates, so it can hold values of any type.

You have already used generic types. [Int] is the short form of Array<Int>, and Set<Int> is the full type of a set of numbers.

Common mistake

The constraint was forgotten:

SWIFT
func larger<T>(_ a: T, _ b: T) -> T {
    a > b ? a : b // ✗
}
TEXT
error: referencing operator function '>' on 'Comparable' requires that 'T' conform to 'Comparable'

The program does not compile, even if larger is only ever called with numbers. The compiler checks the body once and for any T, not for the types of particular calls. Whether any T can be compared with > is unknown, so the comparison is not allowed.

Interviewers like to ask this: why can't a generic function without a constraint compare its arguments? The answer: about an unconstrained T, the compiler knows nothing except that it is a type. The fix is <T: Comparable>.

Key points

  • A generic function or type declares a type parameter in angle brackets, <T>, and Swift fills in the real type each time it is used.
  • The constraint <T: Comparable> lets only types with that protocol into the function and allows the body to compare values.
  • Without a constraint, a > b in the body does not compile, even if the function is only called with Int.
  • Stack<Element>, Array<Int> and Set<Int> are generic types: one design for elements of any type.