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.