Swift: Swift Higher-Order Functions
Higher-order functions are functions that accept other functions as arguments or return functions. The Swift standard library provides powerful higher-order functions like map, filter, and reduce, letting you process collections in a declarative style.
1. What You'll Learn
- Using
mapto transform each element in a collection - Using
filterto select elements that satisfy a condition - Using
reduceto combine a collection into a single value - Using
compactMapandflatMapto handle optionals and nested collections - Chaining multiple higher-order functions for complex data transformations
2. A Real-World Data Analyst Story
(1) Pain Point: Bloated for-Loop Code for Array Processing
Bob needs to extract active users from an array, sort them by age, and format the output strings:
var activeUsers: [String] = []
for user in users {
if user.isActive {
activeUsers.append(user.name)
}
}
activeUsers.sort()
var result: [String] = []
for name in activeUsers {
result.append("User: \(name)")
}
Two for-loops + a temporary array + a sort. The logic is simple but the code is long and filled with "How" details instead of expressing "What."
(2) The Higher-Order Function Solution
Use functional chaining to accomplish everything in one line:
let result = users
.filter { $0.isActive }
.map { $0.name }
.sorted()
.map { "User: \($0)" }
Code goes from 10 lines to 4, with better readability.
(3) Benefit: The Advantages of Declarative Programming
| Dimension | for Loop | Higher-Order Chain |
|---|---|---|
| Code Length | 10-15 lines | 3-5 lines |
| Readability | Read loop body to understand intent | Function names directly express intent |
| Data Flow | Track intermediate variables | Chain clearly shows data flow |
| Change Impact | Rewrite loop for logic changes | Add/remove steps without affecting others |
| Concurrency | Manual management | Inherently parallel-friendly (e.g. parallelMap) |
3. map: Transform Each Element
map applies a transformation to each element in an array and returns a new array of the same length.
graph LR
A["[1, 2, 3, 4, 5]"] --> B["map { $0 * 2 }"]
B --> C["[2, 4, 6, 8, 10]"]
| Scenario | Input | Transform | Output |
|---|---|---|---|
| Double values | [1, 2, 3] |
{ $0 * 2 } |
[2, 4, 6] |
| Extract property | [User, User] |
{ $0.name } |
["Alice", "Bob"] |
| Type conversion | ["1", "2"] |
{ Int($0) } |
[1, 2] (type becomes [Int?]) |
▶ Example: Basic map Usage
// ============================================
// map: Apply transformation to each element
// ============================================
let numbers = [1, 2, 3, 4, 5]
// for-loop approach (old style)
var doubled: [Int] = []
for n in numbers { doubled.append(n * 2) }
// map approach (modern style)
let doubledMap = numbers.map { $0 * 2 }
print("doubledMap: \(doubledMap)")
// Extract string lengths
let words = ["Swift", "Python", "Go"]
let lengths = words.map { $0.count }
print("lengths: \(lengths)")
Output:
TEXT 📖 Display onlydoubledMap: [2, 4, 6, 8, 10] lengths: [5, 6, 2]
4. filter: Select Elements
filter keeps elements that satisfy a condition, returning a new array.
▶ Example: filter Selection Operations
// ============================================
// filter: Keep elements that satisfy a condition
// ============================================
let scores = [45, 82, 91, 63, 77, 55, 88]
// Filter passing scores (>= 60)
let passed = scores.filter { $0 >= 60 }
print("Passed: \(passed)")
// Filter excellent scores (>= 85)
let excellent = scores.filter { $0 >= 85 }
print("Excellent: \(excellent)")
// Chaining: filter failing scores then double them
let failedDoubled = scores
.filter { $0 < 60 }
.map { $0 * 2 }
print("Failed scores doubled: \(failedDoubled)")
Output:
TEXT 📖 Display onlyPassed: [82, 91, 63, 77, 88] Excellent: [91, 88] Failed scores doubled: [90, 110]Tip:
filterreturns a new array preserving the original order.
5. reduce and compactMap
(1) reduce: Combine into a Single Value
reduce combines all elements of an array into a single value.
| Parameter | Meaning | Example |
|---|---|---|
initialResult |
Starting value | 0 for summing, "" for concatenation |
nextPartialResult |
Accumulating closure | { $0 + $1 } |
graph LR
A["[1, 2, 3, 4]"] --> B["reduce(0) { $0 + $1 }"]
B --> C["0+1=1, 1+2=3, 3+3=6, 6+4=10"]
C --> D["10"]
▶ Example: Various reduce Use Cases
// ============================================
// reduce: Reduction operations
// ============================================
let numbers = [1, 2, 3, 4, 5]
// Sum
let sum = numbers.reduce(0) { $0 + $1 }
print("sum: \(sum)")
// Product
let product = numbers.reduce(1) { $0 * $1 }
print("product: \(product)")
// Concatenate strings
let words = ["Swift", "is", "awesome"]
let sentence = words.reduce("") { $0.isEmpty ? $1 : "\($0) \($1)" }
print("sentence: \(sentence)")
// Named parameters for clarity
let sumNamed = numbers.reduce(0) { total, next in total + next }
print("sumNamed: \(sumNamed)")
Output:
TEXT 📖 Display onlysum: 15 product: 120 sentence: Swift is awesome sumNamed: 15
(2) compactMap: Filter nil Values
compactMap works like map but automatically filters out nil values.
// ============================================
// compactMap and flatMap
// ============================================
// compactMap: Filter nil
let strings = ["1", "two", "3", "four", "5"]
let numbers2 = strings.compactMap { Int($0) }
print("numbers: \(numbers2)")
// flatMap: Flatten nested arrays
let nested = [[1, 2], [3, 4, 5], [6]]
let flattened = nested.flatMap { $0 }
print("flattened: \(flattened)")
Output:
TEXT 📖 Display onlynumbers: [1, 3, 5] flattened: [1, 2, 3, 4, 5, 6]
6. Chaining in Practice
The most powerful use of higher-order functions is chaining — connecting multiple operations into a pipeline.
▶ Example: Complex Data Transformation Chain
// ============================================
// Chaining: User data analysis pipeline
// ============================================
struct User {
let name: String
let age: Int
let isActive: Bool
let scores: [Int]
}
let users = [
User(name: "Alice", age: 28, isActive: true, scores: [85, 90, 78]),
User(name: "Bob", age: 22, isActive: false, scores: [70, 75]),
User(name: "Charlie", age: 35, isActive: true, scores: [95, 92, 98, 100]),
User(name: "Diana", age: 17, isActive: true, scores: [60, 65]),
User(name: "Eve", age: 30, isActive: false, scores: [88, 82])
]
// Chain pipeline: active users → adults → average score → sort by score → format
let report = users
.filter { $0.isActive }
.filter { $0.age >= 18 }
.map { user -> (name: String, avgScore: Double) in
let avg = Double(user.scores.reduce(0, +)) / Double(user.scores.count)
return (user.name, avg)
}
.sorted { $0.avgScore > $1.avgScore }
.map { "\($0.name): \(String(format: "%.1f", $0.avgScore))" }
print("=== Active Adult User Report ===")
report.forEach { print($0) }
Output:
TEXT 📖 Display only=== Active Adult User Report === Charlie: 96.2 Alice: 84.3
7. Complete Example: Shopping Cart Price Calculator
// ============================================
// Complete example: Shopping cart price calculator
// Features: map + filter + reduce + compactMap combined
// ============================================
import Foundation
struct CartItem {
let name: String
let price: Double
let quantity: Int
let isDiscounted: Bool
}
let cart = [
CartItem(name: "Laptop", price: 1299, quantity: 1, isDiscounted: false),
CartItem(name: "Mouse", price: 29.99, quantity: 2, isDiscounted: true),
CartItem(name: "Keyboard", price: 99.99, quantity: 1, isDiscounted: true),
CartItem(name: "Cable", price: 9.99, quantity: 3, isDiscounted: false),
CartItem(name: "Monitor", price: 399, quantity: 0, isDiscounted: false)
]
// 1. Filter out zero-quantity items
let validItems = cart.filter { $0.quantity > 0 }
// 2. Calculate total per item (15% off for discounted items)
let itemTotals = validItems.map { item -> (String, Double) in
let unitPrice = item.isDiscounted ? item.price * 0.85 : item.price
let total = unitPrice * Double(item.quantity)
return (item.name, total)
}
// 3. Calculate grand total
let grandTotal = itemTotals.reduce(0) { $0 + $1.1 }
// 4. Format output
print("=== Shopping Cart ===")
itemTotals.forEach { print(" \($0): $\(String(format: "%.2f", $1))") }
print(" ----------")
print(" Total: $\(String(format: "%.2f", grandTotal))")
Output:
TEXT 📖 Display only=== Shopping Cart === Laptop: $1299.00 Mouse: $50.98 Keyboard: $84.99 Cable: $29.97 ---------- Total: $1464.94
❓ FAQ
map returns a new array; forEach only iterates without returning. Use map when you want to transform data into a new array. Use forEach only for side effects (like printing).flatMap flattens nested collections (e.g. [[1,2], [3]] → [1,2,3]), while compactMap filters out nil values (e.g. [1, nil, 3] → [1, 3]). Each has its own role.lazy for deferred evaluation: array.lazy.filter { ... }.map { ... }.reduce simply returns the initial value. For example, [].reduce(0, +) returns 0. However, if the initial value isn't appropriate for empty arrays (e.g. finding a maximum), check isEmpty first.📖 Summary
maptransforms each element and returns a new array of the same lengthfilterkeeps elements matching a condition and returns a new arrayreducecombines a collection into a single value, requiring an initial value and an accumulating closurecompactMaptransforms while filteringnil;flatMapflattens nested collections- Chaining connects multiple higher-order functions into a data pipeline for clean, intent-revealing code
- For large datasets, use
lazyfor deferred evaluation to optimize performance
📝 Exercises
- Basic: Given the integer array
[10, 20, 30, 40, 50], first usemapto divide each element by 2, then usefilterto keep elements greater than 15. - Intermediate: Given the string array
["apple", "banana", "kiwi", "strawberry", "fig"], use chaining to filter strings with length >= 5, convert to uppercase, and sort alphabetically. - Challenge: Given an array of
Transactionstructs (properties:amount: Double,category: String,date: Date), usereduce(into:)to group and sum amounts bycategory. Then output the total for each category.