6. Closures & higher-order functions¶
What & why¶
A closure is a self-contained block of code you can pass around and call later. It can capture variables from the place it was written. Closures power map/filter/sorted, callbacks, SwiftUI, and concurrency (Task { }). A function that takes or returns a function is called higher-order.
Closure syntax¶
Named functions are just closures with a name. The full closure expression:
in separates the signature from the body. The signature can be shortened step by step when passing a closure as an argument:
let numbers = [5, 2, 8, 1]
// Full
let a = numbers.sorted(by: { (x: Int, y: Int) -> Bool in return x < y })
// Types inferred
let b = numbers.sorted(by: { x, y in return x < y })
// Single-expression: implicit return
let c = numbers.sorted(by: { x, y in x < y })
// Shorthand argument names
let d = numbers.sorted(by: { $0 < $1 })
// Trailing closure
let e = numbers.sorted { $0 < $1 }
// Operator method
let f = numbers.sorted(by: <)
All of these are identical; choose the clearest. Use $0 for short, obvious closures, and named parameters when the body is more than a line or two.
Trailing closures¶
If the last argument is a closure, write it after the parentheses:
let doubled = numbers.map { $0 * 2 }
numbers.forEach { n in
print(n)
}
let evenSquares = numbers
.filter { $0.isMultiple(of: 2) }
.map { $0 * $0 }
Functions with several closure parameters allow multiple trailing closures:
func load(onSuccess: (String) -> Void, onFailure: (Error) -> Void) { }
load { text in print(text) } onFailure: { error in print(error) }
Capturing values¶
Closures capture surrounding variables by reference and keep them alive:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counterA = makeCounter()
let counterB = makeCounter()
print(counterA(), counterA(), counterA()) // 1 2 3
print(counterB()) // 1 — independent state
Each call to makeCounter creates a fresh count, and the returned closure owns it. Closures are reference types: copying one shares the same captured state.
Use a capture list to capture a snapshot instead:
Escaping vs non-escaping¶
A closure parameter is non-escaping by default: it must run (or be dropped) before the function returns, which allows optimisations.
If the function stores the closure or calls it later, mark it @escaping:
var handlers: [() -> Void] = []
func register(_ handler: @escaping () -> Void) {
handlers.append(handler) // survives past the call
}
register { print("fired") }
handlers.forEach { $0() }
Closures in classes: avoid retain cycles¶
A class that stores a closure which captures self creates a reference cycle — neither is ever freed. Use [weak self] (or [unowned self]):
class Downloader {
var onFinish: (() -> Void)?
var name = "dl"
func start() {
onFinish = { [weak self] in
guard let self else { return } // unwrap weak reference
print("\(self.name) finished")
}
}
deinit { print("Downloader freed") }
}
var d: Downloader? = Downloader()
d?.start()
d = nil // "Downloader freed" — no leak
Without [weak self], deinit would never run. Details in the ARC lesson. Structs have no such issue (they're copied into closures).
Higher-order functions: the standard toolkit¶
let words = ["swift", "kotlin", "rust", "go"]
words.map { $0.count } // [5, 6, 4, 2]
words.filter { $0.count > 3 } // ["swift", "kotlin", "rust"]
words.reduce(0) { $0 + $1.count } // 17
words.sorted { $0.count < $1.count } // by length
words.first { $0.hasPrefix("r") } // Optional("rust")
words.contains { $0.count == 2 } // true
words.allSatisfy { $0.count >= 2 } // true
words.compactMap { Int($0) } // [] — keeps only non-nil results
[[1, 2], [3]].flatMap { $0 } // [1, 2, 3]
words.min { $0.count < $1.count } // Optional("go")
Dictionary(grouping: words, by: \.count) // [5: ["swift"], 6: ["kotlin"], 4: ["rust"], 2: ["go"]]
Key paths work as functions: words.map(\.count), users.sorted(using: KeyPathComparator(\.age)).
Lazy evaluation¶
Chain without creating intermediate arrays:
let firstBigSquare = (1...).lazy
.map { $0 * $0 }
.first { $0 > 1_000 }
print(firstBigSquare as Any) // Optional(1024)
(1...) is infinite; lazy makes this safe because only as many elements as needed are computed.
Returning and composing functions¶
import Foundation
func compose<A, B, C>(_ f: @escaping (A) -> B, _ g: @escaping (B) -> C) -> (A) -> C {
{ g(f($0)) }
}
let trimThenCount = compose({ (s: String) in s.trimmingCharacters(in: .whitespaces) }, { $0.count })
print(trimThenCount(" hi ")) // 2
Currying-style factories (makeAdder) from the Functions lesson are the same idea.
@autoclosure¶
Wraps an expression in a closure automatically so it's evaluated only if needed:
func log(_ message: @autoclosure () -> String, enabled: Bool) {
if enabled { print(message()) } // the expensive string is built only when enabled
}
log("result: \(expensive())", enabled: false) // expensive() never runs
func expensive() -> Int { 42 }
&&, ||, ??, and assert use this under the hood.
Void and function types¶
| Type | Meaning |
|---|---|
() -> Void |
no params, no result |
(Int) -> Int |
one Int in, Int out |
(String, Int) -> Bool |
two params |
((Int) -> Int) -> Int |
takes a function |
(Int) throws -> Int |
may throw; rethrows for propagating |
@Sendable () -> Void |
safe to call from concurrent code |
Common mistakes¶
- Retain cycles from
selfcaptured strongly in a stored closure. Use[weak self]. - Forgetting
@escapingwhen storing a closure — compiler error "escaping closure captures non-escaping parameter". - Surprising captures: closures capture variables, not values, unless you use a capture list.
- Long closures in a single chain — extract a named function or use named parameters.
- Over-clever
$0/$1in nested closures;$0in an inner closure shadows the outer. - Using
forEachwhere aforloop is clearer —break/continue/returnbehave differently inforEach.
Exercises¶
- Sort
["banana", "kiwi", "apple"]by length (longest first), then alphabetically for ties. - Write
func makeMultiplier(_ n: Int) -> (Int) -> Intand use it to builddoubleandtriple. - Implement your own
myMap,myFilter, andmyReduceas extensions onArray. - Write a
Debouncer-style helper:func once(_ f: @escaping () -> Void) -> () -> Voidthat runsfat most one time. - Demonstrate a retain cycle with a class holding a closure, then fix it with
[weak self]— prove it by watchingdeinitprint. - Use
reduce(into:)to count character frequencies in a string. - Write
composefor three functions, or apipeoperator|>.
Recap¶
- Closures:
{ params in body }, shortened with inference,$0, and trailing-closure syntax. - They capture variables by reference; capture lists (
[x],[weak self]) change that. - Non-escaping by default; mark stored/deferred closures
@escaping. map/filter/reduce/sorted/compactMap/flatMapare the everyday higher-order tools.- Beware retain cycles with classes.