Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

4. Functions and Closures

Chapter 1 showed enough of func to write programs. This chapter finishes the declaration syntax and then spends most of its length on closures, which are functions written where they are used and are how most of Swift’s standard library is called.

4.1 Declaring a Function

Parameters may carry default values, may be variadic, and may be marked modifiable.

// Labels, defaults, variadics, and inout.
func join(_ parts: [String], with separator: String = ", ") -> String {
    var out = ""
    for (i, part) in parts.enumerated() {
        if i > 0 { out += separator }
        out += part
    }
    return out
}

func sum(of numbers: Int...) -> Int {
    var total = 0
    for n in numbers { total += n }
    return total
}

func bump(_ n: inout Int, by amount: Int = 1) {
    n += amount
}

print(join(["a", "b", "c"]))
print(join(["a", "b", "c"], with: " - "))
print(sum(of: 1, 2, 3, 4), sum())

var count = 10
bump(&count)
bump(&count, by: 5)
print(count)
a, b, c
a - b - c
10 0
16

A default value makes its argument optional at the call and lets one function stand in for the two or three overloads another language would need. Put the defaulted parameters last, because a caller supplying one still has to name it.

A variadic parameter, written with ..., arrives in the body as an array. A function may have more than one now, though a call gets ambiguous fast and one is usually enough.

inout hands the function the caller’s variable rather than a copy. The caller writes & at the call site, so the modification is visible in the source and not only in the documentation. An inout argument must be a var, and Swift copies the value in at the start and back out at the end rather than passing an address, which matters only when two arguments refer to the same variable, which the compiler will not let you do.

A function with no arrow returns Void, which is the empty tuple () wearing a friendlier name. A returned value may be ignored, and the compiler warns when you ignore one unless the function is marked @discardableResult.

Argument labels, properly

Chapter 1 introduced the two names a parameter has. The convention that makes Swift read the way it does is that the call site should form a phrase, with the function’s base name carrying the verb and the labels carrying the prepositions. join(parts, with: ", "), sum(of: 1, 2, 3), bump(&count, by: 5). Where the first argument’s role is already obvious from the base name, _ suppresses the label, which is why join takes _ parts and not parts parts.

The labels are part of the function’s name. bump(_:by:) is what that function is called, and a bump(_:to:) beside it is a different function rather than a conflict. This is why Swift needs no overload resolution tricks to have several functions that do related things.

4.2 Functions as Values

A function has a type, written as its parameter types, an arrow, and its return type. Labels are not part of a function type, only of a function’s name, so (Int) -> Int describes twice and square both.

// Functions are values: they have types, and they can be passed and stored.
func twice(_ n: Int) -> Int { n * 2 }
func square(_ n: Int) -> Int { n * n }

func applyAll(_ n: Int, _ steps: [(Int) -> Int]) -> Int {
    var value = n
    for step in steps {
        value = step(value)
    }
    return value
}

let pipeline: [(Int) -> Int] = [twice, square, { $0 - 1 }]
print(applyAll(3, pipeline))

// A nested function is a closure with a name.
func tally(_ words: [String]) -> String {
    var total = 0
    func add(_ w: String) {
        total += w.count
    }
    for w in words {
        add(w)
    }
    return "\(words.count) words, \(total) characters"
}

print(tally(["the", "quick", "brown", "fox"]))
35
4 words, 16 characters

Naming a function without calling it gives you the function itself, so twice goes into the array beside a closure written inline and nothing distinguishes them afterwards. When two functions share a base name, the full name disambiguates: bump(_:by:).

A function declared inside another function is a closure that happens to have a name. add can see and modify total, which is what makes the nested form worth using: the alternative is a parameter and a return value threaded through every call, to say something the enclosing function already knows.

4.3 Closures

A closure is a function without a name, written { parameters in body }. Swift lets you drop everything it can work out for itself, and the standard library is designed on the assumption that you will.

// The same closure, written five ways, each shorter than the last.
let names = ["edsger", "ada", "grace", "alan"]

func byLength(_ a: String, _ b: String) -> Bool {
    return a.count < b.count
}
print(names.sorted(by: byLength))

print(names.sorted(by: { (a: String, b: String) -> Bool in return a.count < b.count }))
print(names.sorted(by: { a, b in a.count < b.count }))
print(names.sorted(by: { $0.count < $1.count }))
print(names.sorted { $0.count < $1.count })

All five print the same line:

["ada", "alan", "grace", "edsger"]

The steps are worth knowing separately, because you will read code written at every one of them. Types go first, since sorted(by:) already says its comparator takes two Strings and returns a Bool. return goes next, because a single-expression closure returns its expression. Then the parameter names themselves, replaced by $0, $1 and so on in order. Last, when the closure is the final argument, it moves outside the parentheses and the label disappears: that is trailing closure syntax, and it is why so much Swift ends in a brace.

A function taking more than one closure can take them all trailing, with the first unlabeled and the rest labeled:

animate(duration: 0.3) {
    view.alpha = 0
} completion: { finished in
    view.removeFromSuperview()
}

Stop shortening before the code stops being readable. $0 < $1 is clear; $0.1.count > $1.1.count is a puzzle, and the version with named parameters is the one to write.

Exercise 4-1. Write applyAll so it takes the steps as a variadic parameter instead of an array, and see what the call looks like.

Exercise 4-2. Sort an array of strings case-insensitively, then by length with ties broken alphabetically.

4.4 Capture

A closure captures the variables it mentions from the scope around it, and it keeps them alive for as long as the closure lives.

// A closure keeps the variables it captured alive.
func makeCounter(startingAt start: Int) -> () -> Int {
    var n = start
    return {
        n += 1
        return n
    }
}

let next = makeCounter(startingAt: 0)
let other = makeCounter(startingAt: 100)

print(next(), next(), next())
print(other(), other())
print(next())
1 2 3
101 102
4

n is a local variable of makeCounter, and makeCounter has returned by the time anything calls the closure. The variable is still there because the closure captured it, and each call to makeCounter makes a fresh one, which is why next and other count independently.

Capture is by reference, not by value: the closure sees the variable, not a snapshot of it, and assigning to n inside the closure changes the same n the enclosing function would see. When you want the snapshot, say so with a capture list:

var total = 0
let report = { [total] in print(total) }
total = 99
report()      // prints 0

The square brackets before in capture each named value by copying it at the moment the closure is created. That is also where [weak self] goes, which Chapter 7 explains along with the reference cycles it prevents.

One consequence catches everybody once. Closures are reference types. Assigning a closure to a second name gives you two names for one closure, sharing the captured state, which is the opposite of how every other value in this chapter behaves and is the subject of Chapter 7.

4.5 Escaping

A closure that outlives the call it was passed to has escaped. Swift assumes a function parameter’s closure does not escape, which lets it skip the bookkeeping that keeping one alive requires, and it checks.

escaping.swift:2:21: error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape
1 | func register(_ handler: () -> Void, into handlers: inout [() -> Void]) {
2 |     handlers.append(handler)
  |                     `- error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape

Storing the closure in an array is the case the rule exists for: the array outlives register, so the closure does too. Marking the parameter @escaping tells the compiler to expect it:

func register(_ handler: @escaping () -> Void, into handlers: inout [() -> Void]) {
    handlers.append(handler)
}

A closure that escapes must say self. explicitly when it refers to a property, which is Swift making a capture of self visible rather than letting it happen quietly. That visibility is the point, and Chapter 7 shows what it is protecting you from.

Exercise 4-3. Write makeAccumulator() that returns a closure taking an Int and returning the running total of everything it has been given.

Exercise 4-4. Rewrite counter.swift using a capture list to copy start, and explain why the program then behaves differently.

Exercise 4-5. Write a function that takes a closure and returns a closure that calls it and prints how long it took. Use it to time a slow function.