Closures and higher-order functions
Unit 6 · Closures & memory. Closures are functions you can pass around and store. This lesson is the syntax and the everyday tools; the next lessons cover what closures capture and the memory consequences.
A closure is a self-contained block of code you can store in a variable, pass to a
function, or return. Swift's collection APIs are built around them — map, filter,
reduce — so reading and writing closures fluently is a daily skill, not a niche one.
Closure syntax, from full to terse
Swift lets you shrink a closure as the context makes its type clear. All four of these do the same thing:
let nums = [1, 2, 3, 4] // 1. Full: explicit parameter and return type let a = nums.map({ (n: Int) -> Int in n * 2 }) // 2. Inferred types — the context knows them let b = nums.map({ n in n * 2 }) // 3. Trailing closure — move it outside the parentheses let c = nums.map { n in n * 2 } // 4. Shorthand argument names — $0 is the first parameter let d = nums.map { $0 * 2 }
Prefer the terse forms when they stay readable. Trailing-closure syntax — the last closure argument moves outside the call's parentheses — is idiomatic Swift and reads especially well when the closure is the star of the call.
Name the parameter when it aids reading. $0 is great for tiny closures like { $0 * 2 }.
Once a closure spans several lines or uses its argument repeatedly, { score in … } is
clearer than a wall of $0s. Terseness is a means, not the goal.
The higher-order toolkit
A higher-order function takes (or returns) another function. These five cover most day-to-day transformations:
let scores = [42, 88, 60, 91, 55] scores.map { $0 + 5 } // [47, 93, 65, 96, 60] — transform each scores.filter { $0 >= 60 } // [88, 60, 91] — keep matching scores.reduce(0, +) // 336 — combine into one scores.sorted(by: >) // [91, 88, 60, 55, 42] — reordered copy let raw = ["3", "x", "7"] raw.compactMap { Int($0) } // [3, 7] — map, dropping nils
maptransforms every element, returning a new array of the results.filterkeeps the elements a predicate returnstruefor.reducefolds the collection into a single value from an initial seed.sortedreturns a reordered copy (the original is untouched — value semantics).compactMapismapthat also discardsnilresults, unwrapping the rest.
Each returns a new value and never mutates the source, so they compose safely.
Chaining into a pipeline
Because each call returns a collection, you can chain them into a top-to-bottom pipeline:
let names = [("Ada", 90), ("Ben", 55), ("Cleo", 72)] let topNames = names .filter { $0.1 >= 60 } // keep passing .sorted { $0.1 > $1.1 } // highest first .map { $0.0 } // project the name // ["Ada", "Cleo"]
Read it as a sentence: keep the passing scores, sort by score descending, take the names. This declarative style states what you want and leaves the looping to the standard library.
Kotlin's collection operators (map, filter, fold, sortedByDescending) line up almost
one-to-one; reduce(0, +) is Kotlin's fold(0) { a, b -> a + b }. Swift's $0 shorthand
plays the role of Kotlin's implicit it. If you're comfortable with Kotlin sequences, this
is a syntax reskin.
Your turn
Build a filter-then-map pipeline.
Knowledge check
Q: What is trailing-closure syntax, and when does it apply?
When a function's last argument is a closure, you can write it outside the call's
parentheses: nums.map { $0 * 2 } instead of nums.map({ $0 * 2 }). It's the idiomatic
form and reads best when the closure is the main argument.
Q: How does compactMap differ from map?
map returns one result per element. compactMap also drops any nil results and unwraps
the rest, so ["3", "x"].compactMap { Int($0) } yields [3] — a non-optional array.