Lesson 08 / 24
Closures
Introduces closure syntax, the `$0` shorthand, the trailing closure and the `sorted(by:)` call; measures that a capture list is evaluated once, when the closure is created.
Published Updated
In this lesson
- Write a closure in short syntax and as a trailing closure
- Show that a closure body runs on every call and that the body of a closure that is never called never runs
- Read the difference between a capture list and capturing without a list
Contents
Functions are named behavior. A closure writes the same behavior without a name, and you can put it in a variable or give it to another function. The question of this lesson has two parts: when does the body of a closure run, and at which moment does the closure see the variables around it?
Writing a Closure
The longest form writes the parameter and return types inside the curly braces: { (a: Int, b: Int) -> Bool in a < b }. The in keyword separates the signature from the body. When the type can be inferred from the context the signature drops, and the parameters are reached by position: $0 is the first and $1 the second argument. If the body is a single expression, return is not written and the value of the expression is returned implicitly.
If the last parameter of a function is a closure, the closure can be written outside the parentheses. This is called a trailing closure. If no other argument is left inside the parentheses, the parentheses themselves drop too: numbers.sorted { $0 > $1 }. sorted(by:) sorts an array according to the comparison closure you give.
// Probe.swift — records when an expression is evaluated
final class Probe {
let label: String
private(set) var evaluations = 0
private(set) var uses = 0
private var evaluatedBeforeUse = false
init(_ label: String) {
self.label = label
}
func eval<T>(_ value: T) -> T {
evaluations += 1
if uses == 0 { evaluatedBeforeUse = true }
return value
}
func use<T>(_ access: () -> T) -> T {
uses += 1
return access()
}
var moment: String {
if evaluations == 0 { return "never" }
if evaluations > 1 { return "on-every-use" }
return evaluatedBeforeUse ? "immediately" : "on-first-use"
}
func report() {
print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
}
}
func tally(_ probes: [Probe]) {
for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
let count = probes.filter { $0.moment == moment }.count
print("\(moment): \(count)")
}
}
The measurement block first runs these forms: the long closure, the short closure with $0, map and our own apply. Then it builds two closures. scale carries a Probe in its body and is called twice; unused is built in the same way but is never called.
// main.swift — closure forms and the moment of the body
let numbers = [3, 1, 2]
print(numbers.sorted(by: { (a: Int, b: Int) -> Bool in a < b }))
print(numbers.sorted { $0 > $1 })
print(numbers.map { $0 * 2 })
func apply(_ n: Int, using transform: (Int) -> Int) -> Int { transform(n) }
print(apply(4) { $0 + 1 })
let bodyProbe = Probe("closure-body")
let unusedProbe = Probe("uncalled-body")
let scale = { (n: Int) -> Int in bodyProbe.eval(n * 2) }
let unused = { (n: Int) -> Int in unusedProbe.eval(n) }
print(bodyProbe.use { scale(1) })
print(bodyProbe.use { scale(2) })
_ = unusedProbe.use { unused }
_ = unusedProbe.use { unused }
let allProbes = [bodyProbe, unusedProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
[1, 2, 3] [3, 2, 1] [6, 2, 4] 5 2 4 closure-body: 2 evaluations, 2 uses -> on-every-use uncalled-body: 0 evaluations, 2 uses -> never immediately: 0 on-first-use: 0 on-every-use: 1 never: 1
sorted(by:) gave the result from smallest to largest, and with $0 > $1 from largest to smallest. map doubled every element, and apply(4) with a trailing closure returned 5. The measurement lines look at the body of the closure: closure-body was evaluated twice in two calls, that is, the body runs again on every call and the moment is on-every-use. uncalled-body showed zero evaluations in two uses: building a closure does not run the body, the body runs only when there is a call.
LB53. How many times the comparison closure of sorted(by:) is called was not counted in this lesson; the number depends on the sorting algorithm, and the lesson shows only the result.
LB54. The closure body was measured with a simple expression that returns only an Int and has no side effects; escaping closures are outside this lesson.
Escaping Closure: Stored Work
If you give a closure to a function and the function stores the closure after it has finished, the type of the closure has to be marked with @escaping. This mark says the closure may escape from the call. A non-escaping closure (the default) disappears when the call ends. An escaping closure can be added to an array, assigned to a property and called later. The line that builds the closure does not do the work; the work is done when the closure is called.
The block below writes a queue class. add stores the closure, and runAll calls all the stored ones. The body of the closure writes to a Probe. The number of evaluations is printed after the closures are added to the queue and after they are run.
// Queue.swift — a stored closure
final class Queue {
var jobs: [() -> Int] = []
func add(_ job: @escaping () -> Int) {
jobs.append(job)
}
func runAll() {
for job in jobs { _ = job() }
}
}
let queueProbe = Probe("stored-job")
let queue = Queue()
queue.add { queueProbe.eval(1) }
queue.add { queueProbe.eval(2) }
print(queueProbe.evaluations)
_ = queueProbe.use { queue.runAll() }
_ = queueProbe.use { queue.runAll() }
print(queueProbe.evaluations)
queueProbe.report()
0 4 stored-job: 4 evaluations, 2 uses -> on-every-use
When the two closures were added to the queue the counter was 0: adding did not run the body. After the two runAll calls the counter became 4, because in each call two closures ran. The closures were stored and evaluated their body again every time they were called; so the moment reads on-every-use.
An escaping closure is the way to postpone work. You read when the body will run not from the line that builds the closure, but from the line that calls it.
LB55. Stored closures were measured inside a class; the consequences of capturing self in an escaping closure are outside this lesson.
Shorthand Argument Names
You can give the closure parameters names like $0 and $1. This spares you from writing the parameter list when the type is inferred from the context. If the body is a single expression, the implicit return works too. The operators of the standard library can also be passed like a function: writing numbers.sorted(by: >) does the same job as numbers.sorted { $0 > $1 }. reduce takes a closure with two parameters, and these parameters become $0 and $1.
The block below runs three shorthand forms on an array. map passes every element through a Probe, and a second map call is applied this time to four elements.
// Shorthand.swift — shorthand arguments
let mapProbe = Probe("map-body")
let numbers = [3, 1, 2]
print(numbers.sorted(by: >))
print(numbers.reduce(0) { $0 + $1 })
print(mapProbe.use { numbers.map { mapProbe.eval($0 * 10) } })
print(mapProbe.use { (numbers + [4]).map { mapProbe.eval($0 * 10) } })
mapProbe.report()
[3, 2, 1] 6 [30, 10, 20] [30, 10, 20, 40] map-body: 7 evaluations, 2 uses -> on-every-use
sorted(by: >) gave [3, 2, 1]. reduce computed the sum as 6. The two map calls produced the arrays [30, 10, 20] and [30, 10, 20, 40]. The Probe showed seven evaluations: three in the first call and four in the second. The closure body ran once for every element. So the moment reads on-every-use.
Shorthand names are readable when the body is short; when the body grows, a named parameter keeps the meaning. Besides $0 and $1, passing the operator directly is the shortest form, but it works only when the signature of the operator matches the signature of the closure.
LB56. Shorthand names were measured with an Int array; $2 and beyond, and the meaning of $0 in nested closures, are outside this lesson.
A Closure Can Change the Variable It Captures
If the captured variable is a var, the closure does not only read it, it can change it. The closure holds not a copy of the variable but the variable itself. If several closures capture the same variable, a change made by one closure changes the value the other sees. For this reason captured var variables behave like shared state, and the lifetime of the variable extends together with the closures that capture it.
The block below captures a hits counter with two closures. record increases the counter by one and passes the value through a Probe; skip increases the counter by two directly. The closures are called in the order record, skip, record.
// Shared.swift — a shared captured variable
let sharedProbe = Probe("shared-counter")
var hits = 0
let record = {
hits += 1
return sharedProbe.eval(hits)
}
let skip = { hits += 2 }
print(sharedProbe.use { record() })
skip()
print(sharedProbe.use { record() })
print(hits)
sharedProbe.report()
1 4 4 shared-counter: 2 evaluations, 2 uses -> on-every-use
record returned 1 on the first call, and 4 on the second call after skip had made the counter 3; the last read confirmed that the value of hits is 4. The two closures saw the same variable: record saw the change skip made on the second call. Two calls brought two evaluations, and the moment is on-every-use.
Shared state gives flexibility, but makes it harder to follow which closure changed the variable and when. When the state is small and close by this is no problem; as it grows, putting the state inside a type becomes more readable.
LB57. The shared state was measured on an Int; closures running concurrently and accessing the same variable are outside this lesson.
Trailing Closure Syntax
If the last parameter of a function is a closure, you can write the closure outside the parentheses. perform(times: 2) { ... } and perform(times: 2, action: { ... }) are the same call. If the only argument of the function is the closure, the parentheses can drop altogether. This syntax only shortens the writing; when the closure runs does not change. The closure runs when the function calls it, and it runs as many times as the function calls it.
The block below writes a function: perform calls the given closure as many times as asked. The closure body writes to a Probe. The function is called first with two, then with three repetitions.
// Trailing.swift — trailing closure
func perform(times: Int, action: () -> Void) {
for _ in 0..<times { action() }
}
let trailingProbe = Probe("trailing-closure")
trailingProbe.use { perform(times: 2) { _ = trailingProbe.eval(1) } }
trailingProbe.use { perform(times: 3) { _ = trailingProbe.eval(1) } }
print(trailingProbe.evaluations)
trailingProbe.report()
5 trailing-closure: 5 evaluations, 2 uses -> on-every-use
perform(times: 2) ran two times and perform(times: 3) three times; the total was five evaluations. Writing the closure outside the parentheses did not change the number of runs: the value of times decided how many times the closure would be called. On each call the body was evaluated again, and the moment reads on-every-use.
This syntax helps only with reading. When the closure is last among the parameters, the call looks like a control-flow statement: perform(times: 3) { ... } reads like a loop. But the rule behind it is the same: a closure is a value, and the function calls it as many times as it wants.
LB58. The trailing closure was measured with a closure that has no return value; the syntax of several trailing closures is outside this lesson.
A Capture List Freezes the Value
When a closure captures a var variable from its surroundings it holds the variable itself; changes made to the variable later are reflected in the closure. If you want to change this, you write a capture list at the start of the closure: { [snapshot = count] in snapshot }. The list takes a copy of the value of count at the moment the closure is created, and the closure now uses that. Even if count changes later, the number the closure sees stays the same.
The block below captures the same variable with two closures: one does not use a list, one does. After the closures are created the variable is changed and both are called. The value read passes through a Probe.
// CaptureList.swift — capture list
let listProbe = Probe("capture-list")
var count = 1
let live = { listProbe.eval(count) }
let frozen = { [snapshot = count] in listProbe.eval(snapshot) }
count = 5
print(listProbe.use { live() }, listProbe.use { frozen() })
listProbe.report()
5 1 capture-list: 2 evaluations, 2 uses -> on-every-use
The live closure gave 5, the frozen closure 1. Because live holds the variable itself, it saw the new value after the assignment count = 5. frozen, on the other hand, took a copy of the value of count at the moment it was created, under the name snapshot; changing the variable later did not reach it. Two calls brought two evaluations, and the moment is on-every-use.
The capture list is the way to write explicitly which value the closure will see. If you want a value to stay as it was when the closure was created, you write it in the list; if you want the closure to see the values the variable takes later, you do not use a list at all.
You can also write several items in a capture list; each item takes its own copy at the moment the closure is created.
LB59. The capture list was measured with an Int variable; capturing class instances weakly is outside this lesson and is covered in a later topic.
The Limiting Measurement
If a closure uses a variable from its surroundings, it captures it. In capturing without a list, the closure is bound to the variable itself: if the variable changes later, the closure sees the new value on every call. A capture list is written inside square brackets and takes a value while the closure is being created: { [snapshot = level] in snapshot }. The expression in the list is evaluated once while the closure is being built, and is not evaluated again at the calls. The measurement below builds two closures with the same variable, changes the variable and calls both of them twice.
// main.swift — capture list
let captureProbe = Probe("capture-list")
var level = 1
let plain = { level }
let listed = { [snapshot = captureProbe.eval(level)] in snapshot }
level = 5
print(captureProbe.use { plain() }, captureProbe.use { listed() })
print(captureProbe.use { plain() }, captureProbe.use { listed() })
let allProbes = [captureProbe]
for probe in allProbes { probe.report() }
tally(allProbes)
5 1 5 1 capture-list: 1 evaluations, 4 uses -> immediately immediately: 1 on-first-use: 0 on-every-use: 0 never: 0
The plain closure saw 5 because it stayed bound to the level variable; the listed closure carried the value 1 it had when it was built. The expression in the capture list was evaluated once, while the closure was being built and before the four uses: the capture-list line gives immediately. The body running on every call and the capture list running once are two separate moments of the same closure.
LB60. level is a var, and the measurement shows only the capture of a value-type variable; capturing class instances was not measured in this lesson.
LB61. That a capture list runs once while the closure is built was measured with a single closure; that in code which builds the same closure in several places each build is a separate evaluation is outside this lesson.
Summary
- A closure is unnamed behavior;
$0and$1reach the arguments by position, and a single-expression body does not needreturn. - If the last parameter is a closure, the closure can be written outside the parentheses (a trailing closure).
- A closure body runs on every call; the body of a closure that is never called never runs.
- A capture list is evaluated once while the closure is built; a later change to a variable captured without a list is visible at the call.
- The moment of this lesson: on every use (the closure body).
Next Step
The examples in this topic used an expression like values.min() ?? 0: min() cannot return a value for an empty array. Swift carries this situation in the type as the absence of a value. The first lesson of the next topic, Optional Values, introduces this idea.
To keep your progress and take notes, Log in
My notes
Log in to take notes.