Swift: Swiftの高階関数:map、filter、reduceと関数チェーン
高階関数とは、他の関数を引数として受け取ったり、関数を返したりする関数です。Swift標準ライブラリはmap、filter、reduceなどの強力な高階関数を提供し、宣言的なスタイルでコレクションを処理できます。
1. 学習目標
mapを使ってコレクションの各要素を変換するfilterを使って条件を満たす要素を選択するreduceを使ってコレクションを単一の値に集約するcompactMapとflatMapを使ってオプショナルとネストコレクションを処理する- 複数の高階関数をチェーンして複雑なデータ変換を行う
2. データアナリストの実話
(1) 課題:配列処理の肥大化したforループコード
Bobはユーザー配列からアクティブユーザーを抽出し、年齢でソートし、出力文字列をフォーマットする必要があります:
SWIFT
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)")
}
forループ2つ + 一時配列 + ソート。ロジックは単純なのにコードは長く、「何をしたいか」ではなく「どうやるか」の詳細で溢れています。
(2) 高階関数による解決策
関数チェーンを使ってすべてを1行で実現します:
SWIFT
let result = users
.filter { $0.isActive }
.map { $0.name }
.sorted()
.map { "User: \($0)" }
コードが10行から4行に減り、可読性も向上します。
(3) 利点:宣言的プログラミングのメリット
| 観点 | forループ | 高階関数チェーン |
|---|---|---|
| コード量 | 10〜15行 | 3〜5行 |
| 可読性 | ループ本文を読まないと意図がわからない | 関数名が直接意図を表現 |
| データフロー | 中間変数を追跡する必要あり | チェーンがデータフローを明示 |
| 変更の影響 | ロジック変更にはループ書き換えが必要 | 他に影響なくステップを追加/削除可能 |
| 並行性 | 手動管理 | 並列化に本質的に適している(例:parallelMap) |
3. map:各要素を変換する
map は配列の各要素に変換を適用し、同じ長さの新しい配列を返します。
graph LR
A["[1, 2, 3, 4, 5]"] --> B["map { $0 * 2 }"]
B --> C["[2, 4, 6, 8, 10]"]
| シナリオ | 入力 | 変換 | 出力 |
|---|---|---|---|
| 値を2倍 | [1, 2, 3] |
{ $0 * 2 } |
[2, 4, 6] |
| プロパティ抽出 | [User, User] |
{ $0.name } |
["Alice", "Bob"] |
| 型変換 | ["1", "2"] |
{ Int($0) } |
[1, 2](型は [Int?] になる) |
▶ サンプル: mapの基本的な使い方
SWIFT
// ============================================
// 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)")
出力:
TEXT 📖 参照専用doubledMap: [2, 4, 6, 8, 10] lengths: [5, 6, 2]
4. filter:要素を選択する
filter は条件を満たす要素を保持し、新しい配列を返します。
▶ サンプル: filterによる選択操作
SWIFT
// ============================================
// 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)")
出力:
TEXT 📖 参照専用Passed: [82, 91, 63, 77, 88] Excellent: [91, 88] Failed scores doubled: [90, 110]ヒント:
filterは元の順序を保持した新しい配列を返します。
5. reduceとcompactMap
(1) reduce:単一の値に集約する
reduce は配列の全要素を単一の値に結合します。
| パラメータ | 意味 | 例 |
|---|---|---|
initialResult |
初期値 | 合計なら 0、連結なら "" |
nextPartialResult |
蓄積クロージャ | { $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"]
▶ サンプル: reduceの様々なユースケース
SWIFT
// ============================================
// 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)")
出力:
TEXT 📖 参照専用sum: 15 product: 120 sentence: Swift is awesome sumNamed: 15
(2) compactMap:nil値をフィルタする
compactMap は map のように動作しますが、nil 値を自動的に除外します。
SWIFT
// ============================================
// 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)")
出力:
TEXT 📖 参照専用numbers: [1, 3, 5] flattened: [1, 2, 3, 4, 5, 6]
6. チェーンの実践
高階関数の最も強力な使い方はチェーンです — 複数の操作をパイプラインとして接続します。
▶ サンプル: 複雑なデータ変換チェーン
SWIFT
// ============================================
// 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) }
出力:
TEXT 📖 参照専用=== Active Adult User Report === Charlie: 96.2 Alice: 84.3
7. 完全な例:ショッピングカート価格計算機
SWIFT
// ============================================
// 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))")
出力:
TEXT 📖 参照専用=== Shopping Cart === Laptop: $1299.00 Mouse: $50.98 Keyboard: $84.99 Cable: $29.97 ---------- Total: $1464.94
❓ よくある質問
Q mapとforEachの違いは何ですか?
A
map は新しい配列を返します。forEach は反復するだけで何も返しません。データを変換して新しい配列が欲しい場合は map を使います。副作用(出力など)だけが必要な場合は forEach を使います。Q flatMapとcompactMapの違いは何ですか?
A Swift 4.1以降、
flatMap はネストコレクションを平坦化し(例:[[1,2], [3]] → [1,2,3])、compactMap は nil 値をフィルタします(例:[1, nil, 3] → [1, 3])。それぞれ役割が異なります。Q チェーンはパフォーマンスに影響しますか?
A 各呼び出しで新しい配列が作成されるため、中間的な割り当てコストが発生します。小〜中規模のデータ(数千要素以下)では影響は無視できます。大規模データセットでは
lazy を使って遅延評価を検討してください:array.lazy.filter { ... }.map { ... }。Q reduceは空配列を処理できますか?
A はい。
reduce は単に初期値を返します。例:[].reduce(0, +) は 0 を返します。ただし、初期値が空配列に適さない場合(最大値を見つけるなど)は、事前に isEmpty をチェックしてください。Q 高階関数ですべてのforループを置き換えられますか?
A ほとんどの場合、はい。ただし、forループが適しているケースもあります:途中でbreakする必要がある場合、複雑な操作にインデックスが必要な場合、隣接要素にアクセスする必要がある場合です。変換、フィルタリング、集約、平坦化には高階関数を使いましょう。
📖 まとめ
mapは各要素を変換し、同じ長さの新しい配列を返すfilterは条件に一致する要素を保持し、新しい配列を返すreduceはコレクションを単一の値に集約し、初期値と蓄積クロージャが必要compactMapは変換しながらnilをフィルタし、flatMapはネストコレクションを平坦化する- チェーンは複数の高階関数をデータパイプラインに接続し、クリーンで意図を表現するコードを実現する
- 大規模データセットでは
lazyを使って遅延評価し、パ���ォーマンスを最適化する
📝 練習問題
- 基本: 整数配列
[10, 20, 30, 40, 50]が与えられたとき、まずmapで各要素を2で割り、次にfilterで15より大きい要素を保持してください。 - 中級: 文字列配列
["apple", "banana", "kiwi", "strawberry", "fig"]が与えられたとき、チェーンを使って長さが5以上の文字列をフィルタし、大文字に変換し、アルファベット順にソートしてください。 - 発展:
Transaction構造体(プロパティ:amount: Double、category: String、date: Date)の配列が与えられたとき、reduce(into:)を使ってカテゴリごとに金額をグループ化して合計してください。その後、各カテゴリの合計を出力してください。