Step 1 of 5 · Reading · ~4 min
Learn
Collections
Higher-Order Methods
A higher-order method is one that takes a function as an argument. Swift's collections are built
almost entirely out of them, and three do most of the work: map transforms, filter selects,
reduce collapses.
let nums = [1, 2, 3, 4, 5]
print(nums.map { $0 * $0 })
print(nums.filter { $0 > 2 })
print(nums.reduce(0, +))
//> [1, 4, 9, 16, 25]
//> [3, 4, 5]
//> 15
| Method | Input | Output | Shape |
|---|---|---|---|
map | [A] | [B] | same count, new values |
filter | [A] | [A] | same values, fewer of them |
reduce | [A] | B | one value out of many |
Why reduce needs a starting value
reduce walks the collection, carrying an accumulator. You must supply what that accumulator
starts as, because the collection might be empty and Swift will not guess.
let nums = [1, 2, 3, 4, 5]
print(nums.reduce(0, +))
print(nums.reduce(0) { acc, n in acc + n })
print(nums.reduce(1, *))
print([Int]().reduce(0, +))
//> 15
//> 15
//> 120
//> 0
The first two lines are the same operation written two ways: + is itself a function of type
(Int, Int) -> Int, so you can hand it over directly instead of writing a closure. The last line
is the payoff — reducing an empty array yields the seed rather than crashing.
Chaining
Each of these returns a new collection, so they compose left to right:
let nums = [1, 2, 3, 4]
print(nums.filter { $0 % 2 == 0 }.map { $0 * $0 }.reduce(0, +))
//> 20
Read it as a pipeline: keep 2 and 4, square them to 4 and 16, add them to 20. Long
chains are idiomatic Swift, but break them across lines once there are more than three stages —
one operation per line is far easier to debug.
compactMap — transform and drop the failures
Any transform that returns an optional pairs naturally with compactMap, which unwraps the
successes and discards the nils in one pass:
let raw = ["1", "2", "abc", "4"]
print(raw.map { Int($0) })
print(raw.compactMap { Int($0) })
//> [Optional(1), Optional(2), nil, Optional(4)]
//> [1, 2, 4]
map gives you [Int?] — the failure is still in there. compactMap gives you [Int]. This is
the standard way to parse a list of strings that might not all be numbers.
Asking questions instead of building collections
Not every higher-order method returns a collection:
let nums = [1, 2, 3, 4, 5]
print(String(describing: nums.first(where: { $0 > 3 })))
print(nums.contains(where: { $0 > 4 }))
print(nums.allSatisfy { $0 > 0 })
print([Int]().allSatisfy { $0 > 0 })
//> Optional(4)
//> true
//> true
//> true
Two things to file away. first(where:) returns an optional, because there may be no match.
And allSatisfy on an empty collection is true — there is no element that breaks the rule, so
the claim holds vacuously. That last one is a genuine source of bugs when a filter unexpectedly
empties a list.
✓ / ✗ — idiom and trap side by side
✓ Idiomatic
let ints = ["3", "x", "7"].compactMap { Int($0) }
print(ints.reduce(0, +))
✗ Same intent, three problems
let ints = ["3", "x", "7"].map { Int($0) }.filter { $0 != nil }.map { $0! }
Three passes over the data instead of one, a force-unwrap that a future edit can invalidate, and
a line nobody reads at a glance. compactMap exists precisely to delete it.
Your exercise
Read a line of space-separated integers, keep the even ones, square them, and print the sum.
The mistake the grader catches is skipping the squaring step. Chaining
.filter { $0 % 2 == 0 }.reduce(0, +) straight through sums the evens themselves — 2 + 4 = 6
on the first visible test, where the answer is 4 + 16 = 20. Put the map between the filter
and the reduce. The hidden test
1 3 5 7 has no even numbers at all and expects 0, which reduce(0, +) handles for free —
but any approach reaching for .max(), .first! or nums[0] on the filtered array crashes
there instead.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…