@k2works/claude-code-booster 6.7.0 → 6.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2048 @@
1
+ ---
2
+ type: Reference
3
+ title: "関数型プログラミングガイド(Kotlin 編)"
4
+ description: "Kotlin の標準ライブラリ、Arrow、kotlinx.coroutines で関数型プログラミングを実践するためのガイド。純粋関数からエラーの型表現、副作用の管理、並行処理、イベントソーシングによるアーキテクチャ設計までを一本の流れで整理する。"
5
+ tags: [reference, kotlin, functional-programming, arrow]
6
+ status: draft
7
+ generated: { by: claude-sonnet-5-5, at: 2026-11-13T00:00:00Z }
8
+ ---
9
+
10
+ # 関数型プログラミングガイド(Kotlin 編)
11
+
12
+ ## このガイドについて
13
+
14
+ 関数型プログラミング(FP)は、状態の書き換えではなく値の変換でプログラムを組み立てる考え方である。変更を楽に安全にできるソフトウェアを作るための技術的な手段のひとつであり、副作用を境界に押し出し、型で失敗を表し、小さな関数を合成する。
15
+
16
+ 本ガイドは、Kotlin でこの考え方を実践するための要点を整理する。次の 2 つの学習記事を元にしている。
17
+
18
+ | 出典 | 内容 | 本ガイドでの扱い |
19
+ | --- | --- | --- |
20
+ | Grokking Functional Programming Kotlin 版(全 12 章) | 純粋関数、イミュータブルなデータ、Option/Either、IO、ストリーム、並行処理、テスト | 第 1〜6 章の骨格 |
21
+ | Zettai Kotlin FP 版(全 13 章、From Objects to Functions 演習) | ToDo アプリを題材にした HTTP、ドメインモデリング、イベントソーシング、CQRS、バリデーション、関数型 JSON | 第 7〜8 章の骨格 |
22
+
23
+ - **対象**: Kotlin の経験があり、Scala を経由せずに JVM 上で実践的な FP を身につけたい開発者。
24
+ - **使うもの**: 第 1〜2 章は Kotlin 標準ライブラリのみ。第 3 章以降は [Arrow](https://arrow-kt.io/) と kotlinx.coroutines も使う。
25
+ - **コード例**: 出典記事の動作確認済みの実装から転記している。本文に載せたコードは、このガイドのために書き下ろしたものではない。詳細な解説と演習問題は出典の記事を参照する。
26
+
27
+ ## 全体像
28
+
29
+ ```mermaid
30
+ flowchart LR
31
+ A[1. 基礎<br>純粋関数] --> B[2. 関数型スタイル<br>不変データと高階関数]
32
+ B --> C[3. 型で表すエラー<br>nullable Option Either]
33
+ C --> D[4. 副作用の管理<br>suspend Flow]
34
+ D --> E[5. 並行処理<br>構造化並行性]
35
+ C --> F[6. 代数構造の実用<br>ファンクタ モナド アプリカティブ]
36
+ F --> G[7. アーキテクチャ<br>イベントソーシング CQRS]
37
+ D --> G
38
+ E --> G
39
+ G --> H[8. テスト戦略<br>スタブ プロパティベース]
40
+ H --> I[9. 静的解析<br>detekt ktlint PMD]
41
+ ```
42
+
43
+ | 章 | 主題 | 一言で言うと |
44
+ | --- | --- | --- |
45
+ | 1 | 基礎 | 同じ入力には同じ出力。外に触れない |
46
+ | 2 | 関数型スタイル | 変更せず、新しい値を作る。関数を値として渡す |
47
+ | 3 | 型で表すエラー | 失敗を例外ではなく戻り値の型で表す |
48
+ | 4 | 副作用の管理 | 副作用を「実行しない記述」として値にする |
49
+ | 5 | 並行処理 | 並行処理も構造と型で安全にする |
50
+ | 6 | 代数構造の実用 | 合成のパターンに名前を付け、判断の手順で使い分ける |
51
+ | 7 | アーキテクチャ | 状態を事実(イベント)の列として扱う |
52
+ | 8 | テスト戦略 | 純粋関数の性質をテストで言い切る |
53
+ | 9 | 静的解析とルール設定 | 関数型の規約を機械で守らせる。PMD のカスタムルールも作る。ただし教材コードとの衝突に注意する |
54
+
55
+ ---
56
+
57
+ ## 1. 基礎: 純粋関数
58
+
59
+ ### 1.1 命令型と関数型
60
+
61
+ 命令型は「どうやるか」の手順を書き、関数型は「何であるか」の定義を書く。
62
+
63
+ ```kotlin
64
+ // Kotlin: 命令型でワードスコアを計算
65
+ fun calculateScoreImperative(word: String): Int {
66
+ var score = 0
67
+ for (c in word) {
68
+ score++
69
+ }
70
+ return score
71
+ }
72
+ ```
73
+
74
+ ```kotlin
75
+ // Kotlin: 関数型でワードスコアを計算
76
+ fun wordScore(word: String): Int = word.length
77
+ ```
78
+
79
+ | 概念 | 命令型 | 関数型 |
80
+ | --- | --- | --- |
81
+ | 焦点 | 手順(How) | 結果(What) |
82
+ | 状態 | 変更する(`var`) | 変換する(`val`) |
83
+ | ループ | `for` / `while` | `map` / `filter` / `fold` |
84
+ | データ | ミュータブル(`MutableList`) | イミュータブル(`List`) |
85
+ | 関数の形 | ブロック本体 + `return` | 式本体関数 |
86
+
87
+ Kotlin は、トップレベル関数、式本体関数、`val`、式としての `if` / `when` を言語機能として備えている。式本体関数は「値を計算する式」であることが一目で分かり、`return` や途中の代入を書く余地がないため副作用が紛れ込みにくい。
88
+
89
+ ### 1.2 純粋関数
90
+
91
+ 純粋関数は次の 2 つを満たす。
92
+
93
+ 1. 同じ入力には常に同じ出力を返す。
94
+ 2. 副作用がない(外部状態を変更しない)。
95
+
96
+ ```kotlin
97
+ // 純粋関数の例
98
+ fun increment(x: Int): Int = x + 1
99
+
100
+ fun add(a: Int, b: Int): Int = a + b
101
+
102
+ fun getFirstCharacter(s: String): Char = s[0]
103
+
104
+ /** 入力の 95% を返す */
105
+ fun applyDiscount(x: Double): Double = x * 95.0 / 100.0
106
+ ```
107
+
108
+ 式本体で書くことは純粋さの手がかりにはなるが、保証にはならない。乱数やシステム時計は **隠れた入力** であり、引数が同じでも結果が変わる。
109
+
110
+ ```kotlin
111
+ import kotlin.random.Random
112
+
113
+ /** 不純: 乱数に依存するため、同じ入力でも毎回異なる値を返しうる */
114
+ fun randomPart(x: Double): Double = x * Random.nextDouble()
115
+
116
+ /** 不純: 呼び出すタイミングによって結果が変わる */
117
+ fun currentTime(): Long = System.currentTimeMillis()
118
+ ```
119
+
120
+ ### 1.3 状態を持つクラスの問題
121
+
122
+ 状態をクラスに持たせると、状態同士の食い違いが生まれる。次のカートは `items` が外から変更でき、`bookAdded` と食い違う。
123
+
124
+ ```kotlin
125
+ class ShoppingCartBad {
126
+ /** 問題: 可変リストを外部に公開しているため、誰でも中身を変更できる */
127
+ val items: MutableList<String> = mutableListOf()
128
+
129
+ /** 問題: items とは別に管理されるフラグ。items と食い違う可能性がある */
130
+ private var bookAdded = false
131
+
132
+ fun addItem(item: String) {
133
+ items.add(item)
134
+ if (item == "Book") {
135
+ bookAdded = true
136
+ }
137
+ }
138
+
139
+ fun getDiscountPercentage(): Int = if (bookAdded) 5 else 0
140
+
141
+ /** 読み取り専用の型で返しても、中身は内部の可変リストそのもの */
142
+ fun itemsView(): List<String> = items
143
+ }
144
+ ```
145
+
146
+ 状態をやめて、中身を引数で受け取る純粋関数にすると、食い違いの余地が最初からなくなる。
147
+
148
+ ```kotlin
149
+ /** Book が含まれていれば 5%、そうでなければ 0% の割引率を返す */
150
+ fun getDiscountPercentage(items: List<String>): Int =
151
+ if ("Book" in items) 5 else 0
152
+ ```
153
+
154
+ **純粋関数の利点**: テストが入力と出力の確認だけで済む。並行に呼んでも安全である。呼び出しを値に置き換えても意味が変わらない(参照透過性)ので、読んで理解しやすい。
155
+
156
+ ---
157
+
158
+ ## 2. 関数型スタイル: 不変データと高階関数
159
+
160
+ ### 2.1 読み取り専用 ≠ イミュータブル
161
+
162
+ Kotlin の `List` は **読み取り専用のビュー** であり、イミュータブルを保証する型ではない。元の `MutableList` が変更されると、`List` として参照している側にも見える。
163
+
164
+ ```kotlin
165
+ test("MutableList を List として参照しても、元のリストの変更は見えてしまう") {
166
+ val mutable = mutableListOf("a", "b")
167
+ val readOnly: List<String> = mutable
168
+
169
+ mutable.add("c")
170
+
171
+ readOnly shouldBe listOf("a", "b", "c")
172
+ }
173
+ ```
174
+
175
+ 可変なコレクションを共有せず、必要なら呼び出し時点でコピーを取る。変更は新しいリストを作ることで表す。
176
+
177
+ ```kotlin
178
+ /** 呼び出し時点の内容を新しいリストとしてコピーする */
179
+ fun <T> snapshotOf(list: List<T>): List<T> = list.toList()
180
+ ```
181
+
182
+ ```kotlin
183
+ val appleBook = listOf("Apple", "Book")
184
+ val appleBookMango = appleBook + "Mango"
185
+
186
+ appleBook shouldBe listOf("Apple", "Book") // 元のリストは変わらない
187
+ appleBookMango shouldBe listOf("Apple", "Book", "Mango") // 新しいリストが作成される
188
+ ```
189
+
190
+ 局所的にどうしても可変操作が必要なら、`buildList` の内側に閉じ込める。
191
+
192
+ ```kotlin
193
+ /** 1 から n までの 2 乗のリストを作る(可変操作は buildList の内側に閉じ込める) */
194
+ fun squares(n: Int): List<Int> = buildList {
195
+ for (i in 1..n) add(i * i)
196
+ }
197
+ ```
198
+
199
+ `data class` の `copy` も同じ考え方で、元を変えずに一部だけ違う新しい値を作る。
200
+
201
+ ```kotlin
202
+ data class ProgrammingLanguage(val name: String, val year: Int)
203
+
204
+ /** 名前だけを変えた新しいインスタンスを返す */
205
+ fun renamed(language: ProgrammingLanguage, newName: String): ProgrammingLanguage =
206
+ language.copy(name = newName)
207
+ ```
208
+
209
+ ### 2.2 関数を値として渡す
210
+
211
+ 関数は引数にも戻り値にもなる。変わる部分を関数として外から渡すと、同じ枠組みで振る舞いを差し替えられる。
212
+
213
+ ```kotlin
214
+ /** 基本スコア: 'a' を除いた文字数 */
215
+ fun score(word: String): Int = word.replace("a", "").length
216
+
217
+ fun bonus(word: String): Int = if (word.contains("c")) 5 else 0
218
+ fun penalty(word: String): Int = if (word.contains("s")) 7 else 0
219
+
220
+ /** スコア関数を受け取ってランキングを作成 */
221
+ fun rankedWords(wordScore: (String) -> Int, words: List<String>): List<String> =
222
+ words.sortedBy(wordScore).reversed()
223
+
224
+ val words = listOf("ada", "haskell", "scala", "java", "rust")
225
+
226
+ rankedWords(::score, words) shouldBe listOf("haskell", "rust", "scala", "java", "ada")
227
+ rankedWords({ w -> score(w) + bonus(w) - penalty(w) }, words) shouldBe
228
+ listOf("java", "scala", "ada", "haskell", "rust")
229
+ ```
230
+
231
+ 関数参照(`::score`)、ラムダ、末尾ラムダ、暗黙のパラメータ `it` は、同じ意味の別の書き方である。
232
+
233
+ ```kotlin
234
+ // 以下はすべて同じ意味
235
+ sortByFunction(words, { s -> s.length })
236
+ sortByFunction(words) { s -> s.length }
237
+ sortByFunction(words) { it.length }
238
+ ```
239
+
240
+ 拡張関数にすると、末尾ラムダでパイプラインのように読める。
241
+
242
+ ```kotlin
243
+ /** 拡張関数版のランキング - 末尾ラムダでスコア関数を渡せる */
244
+ fun List<String>.rankedBy(wordScore: (String) -> Int): List<String> =
245
+ sortedBy(wordScore).reversed()
246
+
247
+ words.rankedBy { score(it) + bonus(it) } shouldBe listOf("scala", "haskell", "rust", "java", "ada")
248
+ ```
249
+
250
+ ### 2.3 map / filter / fold
251
+
252
+ | 操作 | 役割 | 例 |
253
+ | --- | --- | --- |
254
+ | `map` | 各要素を変換する(要素数は同じ) | `words.map(::len)` |
255
+ | `filter` | 条件に合う要素だけ残す | `numbers.filter(::isOdd)` |
256
+ | `fold` | 初期値から畳み込んで 1 つの値にする | 合計、最大値、連結 |
257
+
258
+ ```kotlin
259
+ /** 合計を計算 */
260
+ fun sum(numbers: List<Int>): Int = numbers.fold(0) { acc, i -> acc + i }
261
+
262
+ /** 最大値を取得(空リストなら Int.MIN_VALUE) */
263
+ fun largest(numbers: List<Int>): Int = numbers.fold(Int.MIN_VALUE) { max, i -> if (i > max) i else max }
264
+ ```
265
+
266
+ 空のリストでは、`fold` は初期値を返し、`reduce` は例外を投げる。初期値が自然に決まらない場合は、結果を nullable で返す `reduceOrNull` を使う。
267
+
268
+ ```kotlin
269
+ sum(emptyList()) shouldBe 0 // fold は初期値を返す
270
+ shouldThrow<UnsupportedOperationException> {
271
+ emptyList<Int>().reduce { acc, i -> acc + i } // reduce は例外を投げる
272
+ }
273
+ ```
274
+
275
+ ```kotlin
276
+ /** 最大値を取得(空リストなら null) */
277
+ fun largestOrNull(numbers: List<Int>): Int? = numbers.reduceOrNull { max, i -> if (i > max) i else max }
278
+ ```
279
+
280
+ ### 2.4 関数を返す関数とカリー化
281
+
282
+ 条件を関数として組み立てて返すと、同じ `filter` に別の条件を渡せる。
283
+
284
+ ```kotlin
285
+ /** 指定値より大きいかを判定する関数を返す */
286
+ fun largerThan(n: Int): (Int) -> Boolean = { i -> i > n }
287
+
288
+ /** 指定値で割り切れるかを判定する関数を返す */
289
+ fun divisibleBy(n: Int): (Int) -> Boolean = { it % n == 0 }
290
+
291
+ val numbers = listOf(5, 1, 2, 4, 0)
292
+ numbers.filter(largerThan(4)) shouldBe listOf(5)
293
+ numbers.filter(divisibleBy(2)) shouldBe listOf(2, 4, 0)
294
+ ```
295
+
296
+ 複数の引数を 1 つずつ受け取る形(カリー化)にすると、先頭の引数だけ与えた部分適用で、新しい関数を作れる。
297
+
298
+ ```kotlin
299
+ /** 2 引数関数をカリー化する */
300
+ fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C = { a -> { b -> f(a, b) } }
301
+ ```
302
+
303
+ ### 2.5 flatMap
304
+
305
+ `map` で「リストを返す関数」を適用すると結果がネストする。`flatMap` は適用と平坦化を一度に行う。
306
+
307
+ ```kotlin
308
+ data class Book(val title: String, val authors: List<String>)
309
+
310
+ /** 全著者のリストを取得 */
311
+ fun allAuthors(books: List<Book>): List<String> = books.flatMap(Book::authors)
312
+
313
+ val books = listOf(
314
+ Book("FP in Scala", listOf("Chiusano", "Bjarnason")),
315
+ Book("The Hobbit", listOf("Tolkien")),
316
+ )
317
+
318
+ // map だけだとリストがネストする
319
+ books.map(Book::authors) shouldBe listOf(listOf("Chiusano", "Bjarnason"), listOf("Tolkien"))
320
+
321
+ // flatMap で平坦化
322
+ allAuthors(books) shouldBe listOf("Chiusano", "Bjarnason", "Tolkien")
323
+ ```
324
+
325
+ `flatMap` が返すリストの大きさは自由で、要素数を増やすことも、減らす(`filter` の代わりにする)こともできる。
326
+
327
+ ```kotlin
328
+ /** 各要素を i と i + 10 の 2 つにする(要素数が増える) */
329
+ fun duplicate(numbers: List<Int>): List<Int> = numbers.flatMap { listOf(it, it + 10) }
330
+
331
+ /** 偶数のみを残す(要素数が減る - filter の代わり) */
332
+ fun filterEvens(numbers: List<Int>): List<Int> =
333
+ numbers.flatMap { if (it % 2 == 0) listOf(it) else emptyList() }
334
+ ```
335
+
336
+ `flatMap` を入れ子にすると深くなる。Kotlin には Scala の for 内包表記に当たる構文がないため、`sequence { }` ビルダーで同じ形を平らに書ける。
337
+
338
+ ```kotlin
339
+ /** おすすめ文を生成(ネストした flatMap) */
340
+ fun recommendations(books: List<Book>): List<String> =
341
+ books.flatMap { book ->
342
+ book.authors.flatMap { author ->
343
+ bookAdaptations(author).map { movie ->
344
+ "You may like ${movie.title}, because you liked $author's ${book.title}"
345
+ }
346
+ }
347
+ }
348
+ ```
349
+
350
+ ```kotlin
351
+ /** おすすめ文を生成(sequence ビルダー版) */
352
+ fun recommendationsWithSequence(books: List<Book>): List<String> =
353
+ sequence {
354
+ for (book in books)
355
+ for (author in book.authors)
356
+ for (movie in bookAdaptations(author))
357
+ yield("You may like ${movie.title}, because you liked $author's ${book.title}")
358
+ }.toList()
359
+ ```
360
+
361
+ ---
362
+
363
+ ## 3. 型で表すエラー
364
+
365
+ 例外は関数のシグネチャに現れず、呼び出し側が処理を忘れても気づけない。失敗の可能性を戻り値の型に出すと、扱いをコンパイラが強制する。
366
+
367
+ ### 3.1 nullable 型
368
+
369
+ 値がないことだけを表すなら、Kotlin 標準の nullable 型(`A?`)で十分である。`?:`(エルビス演算子)、`?.let`、`toIntOrNull` で合成できる。
370
+
371
+ ```kotlin
372
+ fun safeDivide(a: Int, b: Int): Int? = if (b == 0) null else a / b
373
+
374
+ fun addStrings(a: String, b: String): Int? {
375
+ val x = a.trim().toIntOrNull() ?: return null
376
+ val y = b.trim().toIntOrNull() ?: return null
377
+ return x + y
378
+ }
379
+ ```
380
+
381
+ 例外を投げる実装を、小さな nullable 関数の合成に置き換える。
382
+
383
+ ```kotlin
384
+ fun parseShow(rawShow: String): TvShow? {
385
+ val name = extractName(rawShow) ?: return null
386
+ val yearStart = extractYearStart(rawShow) ?: extractSingleYear(rawShow) ?: return null
387
+ val yearEnd = extractYearEnd(rawShow) ?: extractSingleYear(rawShow) ?: return null
388
+ return TvShow(name, yearStart, yearEnd)
389
+ }
390
+ ```
391
+
392
+ ```kotlin
393
+ // 正常ケース
394
+ parseShow("Breaking Bad (2008-2013)") // TvShow("Breaking Bad", 2008, 2013)
395
+
396
+ // 異常ケース → null が返される(例外なし)
397
+ parseShow("The Wire 2002-2008") // null
398
+ ```
399
+
400
+ `?:` は「左が null なら右を使う」フォールバックである。
401
+
402
+ ### 3.2 Either: 失敗の理由を持つ
403
+
404
+ nullable は「なぜ失敗したか」を運べない。理由を持たせるときは Arrow の `Either<E, A>` を使う。慣例で左(`Left`)が失敗、右(`Right`)が成功である。
405
+
406
+ ```kotlin
407
+ fun extractName(rawShow: String): Either<String, String> {
408
+ val bracketOpen = rawShow.indexOf('(')
409
+ return if (bracketOpen > 0) {
410
+ rawShow.substring(0, bracketOpen).trim().right()
411
+ } else {
412
+ "Can't extract name from: $rawShow".left()
413
+ }
414
+ }
415
+
416
+ /** nullable 型の結果に失敗理由を付けて Either にする */
417
+ fun parseYear(yearStr: String, context: String): Either<String, Int> =
418
+ yearStr.trim().toIntOrNull()?.right() ?: "Can't parse $context: '$yearStr'".left()
419
+ ```
420
+
421
+ ```kotlin
422
+ extractName("The Wire (2002-2008)") // Right("The Wire")
423
+ extractName("(2022)") // Left("Can't extract name from: (2022)")
424
+ ```
425
+
426
+ ### 3.3 either { } DSL と Raise
427
+
428
+ `flatMap` の入れ子は、`either { }` と `bind()` で逐次コードのように書ける。`bind()` は `Left` なら即座にブロックを抜ける。
429
+
430
+ ```kotlin
431
+ import arrow.core.raise.either
432
+
433
+ /** either { } DSL で組み立てる */
434
+ fun parseShow(rawShow: String): Either<String, TvShow> = either {
435
+ val name = extractName(rawShow).bind()
436
+ val yearStart = extractYearStart(rawShow).getOrElse { extractSingleYear(rawShow).bind() }
437
+ val yearEnd = extractYearEnd(rawShow).getOrElse { extractSingleYear(rawShow).bind() }
438
+ TvShow(name, yearStart, yearEnd)
439
+ }
440
+ ```
441
+
442
+ `Raise<E>` をレシーバにすると、「この関数は `E` で失敗しうる」ことをシグネチャで示せる。`ensure` が条件を検査し、満たさなければ中断する。
443
+
444
+ ```kotlin
445
+ import arrow.core.raise.Raise
446
+ import arrow.core.raise.ensure
447
+
448
+ /** Raise<String> の文脈で年齢を検証する。失敗したら raise で処理を中断する */
449
+ fun Raise<String>.checkAge(age: Int): Int {
450
+ ensure(age >= 0) { "Age cannot be negative" }
451
+ ensure(age <= 150) { "Age cannot be greater than 150" }
452
+ return age
453
+ }
454
+
455
+ /** either { } で Raise の関数を Either を返す関数に変換する */
456
+ fun validateAge(age: Int): Either<String, Int> = either { checkAge(age) }
457
+ ```
458
+
459
+ `Raise` の関数は `bind()` なしで直接呼び出して合成できる。
460
+
461
+ ```kotlin
462
+ /** Raise の関数を直接呼び出して組み立てる(bind() も不要) */
463
+ fun validateUserRaise(username: String, email: String, age: Int): Either<String, User> = either {
464
+ User(checkUsername(username), checkEmail(email), checkAge(age))
465
+ }
466
+ ```
467
+
468
+ ### 3.4 エラーを集める
469
+
470
+ `either { }` は最初のエラーで止まる。入力検証のように全部のエラーを返したいときは、`zipOrAccumulate` で `NonEmptyList` に集める。
471
+
472
+ ```kotlin
473
+ import arrow.core.NonEmptyList
474
+ import arrow.core.raise.zipOrAccumulate
475
+
476
+ /** zipOrAccumulate: すべての検証を実行し、エラーを NonEmptyList に集める */
477
+ fun validateUserAccumulating(username: String, email: String, age: Int): Either<NonEmptyList<String>, User> =
478
+ either {
479
+ zipOrAccumulate(
480
+ { checkUsername(username) },
481
+ { checkEmail(email) },
482
+ { checkAge(age) },
483
+ ) { validUsername, validEmail, validAge ->
484
+ User(validUsername, validEmail, validAge)
485
+ }
486
+ }
487
+ ```
488
+
489
+ リストを処理するときの失敗の扱いは、次の 3 つの戦略から選ぶ。
490
+
491
+ ```kotlin
492
+ /** Best-effort 戦略: パースできたものだけを返す */
493
+ fun parseShowsBestEffort(rawShows: List<String>): List<TvShow> =
494
+ rawShows.mapNotNull { parseShow(it).getOrNull() }
495
+
496
+ /** All-or-nothing 戦略: 最初のエラーで停止する */
497
+ fun parseShowsAllOrNothing(rawShows: List<String>): Either<String, List<TvShow>> = either {
498
+ rawShows.map { parseShow(it).bind() }
499
+ }
500
+ ```
501
+
502
+ もう 1 つはエラー収集戦略で、すべてのエラーを `NonEmptyList` に集める。
503
+
504
+ ### 3.5 sealed interface で ADT を作る
505
+
506
+ 取りうる場合を限定した型(代数的データ型)を `sealed interface` で表し、`when` で網羅的に処理する。場合を追加したとき、扱いを忘れている `when` はコンパイルエラーになる。
507
+
508
+ ```kotlin
509
+ sealed interface ToDoListEvent {
510
+ val user: User
511
+ val listName: ListName
512
+ }
513
+
514
+ data class ListCreated(
515
+ override val user: User,
516
+ override val listName: ListName,
517
+ ) : ToDoListEvent
518
+
519
+ data class ItemAdded(
520
+ override val user: User,
521
+ override val listName: ListName,
522
+ val item: ToDoItem,
523
+ ) : ToDoListEvent
524
+ ```
525
+
526
+ ### 3.6 使い分け
527
+
528
+ | 状況 | 使うもの |
529
+ | --- | --- |
530
+ | 値がないことだけを表したい | nullable 型(`A?`)と `?:` |
531
+ | 失敗の理由を呼び出し側に伝えたい | `Either<E, A>` |
532
+ | 逐次的に書きたい/関数のシグネチャで失敗を示したい | `either { }` と `Raise<E>` |
533
+ | すべての検証エラーをまとめて返したい | `zipOrAccumulate` と `NonEmptyList` |
534
+ | 取りうる場合を限定して漏れを防ぎたい | `sealed interface` と `when` |
535
+
536
+ Arrow の `Option` は nullable の代替だが、Kotlin では nullable 型で足りる場面が多い。Arrow の API と合成したいときなど、`Option` が必要な場面に限って使う。
537
+
538
+ ---
539
+
540
+ ## 4. 副作用の管理
541
+
542
+ ### 4.1 副作用を値として扱う
543
+
544
+ 副作用のある処理を「実行しない記述」として値にすると、いつ実行するかを呼び出し側が決められる。最小の形は、引数なしの関数(サンク)である。
545
+
546
+ ```kotlin
547
+ /** 呼び出すたびに異なる値が返る */
548
+ fun castTheDieImpure(): Int = Random.nextInt(1, 7)
549
+
550
+ /** サンク: 「サイコロを振る」という計算の記述を返す */
551
+ fun castTheDieThunk(): () -> Int = { castTheDieImpure() }
552
+ ```
553
+
554
+ 出典の記事では、このサンクを包む小さな `IO<A>` 型を実装して、`map` / `flatMap` / `orElse` / `retry` を定義している。
555
+
556
+ ```kotlin
557
+ class IO<A> private constructor(private val thunk: () -> A) {
558
+
559
+ /** IO を実行して結果を取得する(ここで初めて副作用が発生する) */
560
+ fun unsafeRun(): A = thunk()
561
+
562
+ /** 結果を変換する */
563
+ fun <B> map(f: (A) -> B): IO<B> = IO { f(thunk()) }
564
+
565
+ /** IO を返す関数を適用してフラット化する */
566
+ fun <B> flatMap(f: (A) -> IO<B>): IO<B> = IO { f(thunk()).unsafeRun() }
567
+
568
+ companion object {
569
+ /** 副作用のある式を遅延実行する IO を作る */
570
+ fun <A> delay(thunk: () -> A): IO<A> = IO(thunk)
571
+
572
+ /** 既存の値をラップする(副作用なし) */
573
+ fun <A> pure(value: A): IO<A> = IO { value }
574
+ }
575
+ }
576
+ ```
577
+
578
+ ### 4.2 Kotlin では suspend が IO の役割を担う
579
+
580
+ Kotlin では、自前の `IO` 型を作らなくても `suspend` 関数が副作用の型付けになる。`suspend` 関数はコルーチンの中でしか呼べないので、副作用を持つ処理の呼び出し位置が限定される。`suspend` ラムダは値として保持でき、呼び出すまで実行されない。
581
+
582
+ ```kotlin
583
+ /** suspend: 呼び出せる場所がコルーチンの中に限定される */
584
+ suspend fun castTheDie(): Int = castTheDieImpure()
585
+ ```
586
+
587
+ ```kotlin
588
+ test("suspend ラムダは値として保持でき、呼び出すまで実行されない") {
589
+ var calls = 0
590
+ val program: suspend () -> Int = { calls += 1; castTheDie() }
591
+ calls shouldBe 0
592
+ program() shouldBeInRange 1..6
593
+ program() shouldBeInRange 1..6
594
+ calls shouldBe 2
595
+ }
596
+ ```
597
+
598
+ 合成は `flatMap` ではなく、普通の逐次コードで書ける。
599
+
600
+ ```kotlin
601
+ /** flatMap の代わりに、普通の逐次コードとして合成できる */
602
+ suspend fun castTheDieTwice(): Int {
603
+ val first = castTheDie()
604
+ val second = castTheDie()
605
+ return first + second
606
+ }
607
+ ```
608
+
609
+ ブロッキングの I/O は `withContext(Dispatchers.IO)` に閉じ込める。
610
+
611
+ ```kotlin
612
+ suspend fun calendarEntries(name: String): List<MeetingTime> =
613
+ withContext(Dispatchers.IO) { calendarEntriesApiCall(name) }
614
+
615
+ suspend fun createMeeting(names: List<String>, meeting: MeetingTime): Unit =
616
+ withContext(Dispatchers.IO) { createMeetingApiCall(names, meeting) }
617
+ ```
618
+
619
+ ### 4.3 失敗とリトライ
620
+
621
+ 例外は `Either.catch` で値にし、フォールバックは `getOrElse` で書く。再実行は Arrow の `Schedule` で表す。
622
+
623
+ ```kotlin
624
+ /** suspend ラムダ版の orElse: 失敗したら fallback を実行する新しい「記述」を返す */
625
+ infix fun <A> (suspend () -> A).orElse(fallback: suspend () -> A): suspend () -> A = {
626
+ Either.catch { this() }.getOrElse { fallback() }
627
+ }
628
+ ```
629
+
630
+ ```kotlin
631
+ /** Arrow の Schedule で最大 maxRetries 回まで再実行する */
632
+ suspend fun <A> retry(maxRetries: Int, action: suspend () -> A): A =
633
+ Schedule.recurs<Throwable>(maxRetries.toLong()).retry(action)
634
+
635
+ /** リトライしても全て失敗したらデフォルト値を返す */
636
+ suspend fun <A> retryWithDefault(maxRetries: Int, default: A, action: suspend () -> A): A =
637
+ Either.catch { retry(maxRetries, action) }.getOrElse { default }
638
+ ```
639
+
640
+ ### 4.4 副作用を境界に寄せる
641
+
642
+ 純粋な計算と副作用を分け、副作用は関数として外から受け取る。次のスケジューリングでは、空き時間の計算が純粋関数で、取得と作成だけが副作用である。
643
+
644
+ ```kotlin
645
+ fun meetingsOverlap(meeting1: MeetingTime, meeting2: MeetingTime): Boolean =
646
+ meeting1.endHour > meeting2.startHour && meeting2.endHour > meeting1.startHour
647
+
648
+ fun possibleMeetings(
649
+ existingMeetings: List<MeetingTime>,
650
+ startHour: Int,
651
+ endHour: Int,
652
+ lengthHours: Int,
653
+ ): List<MeetingTime> =
654
+ (startHour..endHour - lengthHours)
655
+ .map { start -> MeetingTime(start, start + lengthHours) }
656
+ .filter { slot -> existingMeetings.none { meetingsOverlap(it, slot) } }
657
+ ```
658
+
659
+ 入出力を関数として受け取れば、コンソールでもテスト用のスタブでも差し替えられる。
660
+
661
+ ```kotlin
662
+ /** 入出力を関数として受け取ることで、コンソールにもテスト用スタブにも差し替えられる */
663
+ suspend fun schedulingProgram(
664
+ getName: suspend () -> String,
665
+ showMeeting: suspend (MeetingTime?) -> Unit,
666
+ ) {
667
+ val name1 = getName()
668
+ val name2 = getName()
669
+ val possibleMeeting = schedule(listOf(name1, name2), 2)
670
+ showMeeting(possibleMeeting)
671
+ }
672
+ ```
673
+
674
+ ### 4.5 ストリーム: Sequence と Flow
675
+
676
+ 連続したデータを遅延評価で扱う。必要な要素だけが評価されるので、無限のストリームも定義できる。
677
+
678
+ | 型 | 評価 | 副作用(suspend) | 向く場面 |
679
+ | --- | --- | --- | --- |
680
+ | `Sequence<A>` | 遅延・同期 | 呼び出せない | メモリ上の変換、無限列の定義 |
681
+ | `Flow<A>` | 遅延・非同期(コールド) | 呼び出せる | I/O を伴う連続した値 |
682
+
683
+ ```kotlin
684
+ /** 自然数の無限ストリーム */
685
+ val naturals: Sequence<Int> = generateSequence(1) { it + 1 }
686
+
687
+ naturals.filter { it % 2 == 0 }.take(3).toList() // [2, 4, 6]
688
+ ```
689
+
690
+ `Flow` はコールドで、`collect` するたびに最初から実行される。
691
+
692
+ ```kotlin
693
+ /** suspend 関数 castTheDie を無限に呼び出すストリーム */
694
+ fun infiniteDieCasts(): Flow<Int> = flow {
695
+ while (true) emit(castTheDie())
696
+ }
697
+
698
+ /** 6 が出るまで振り続け、6 を含めた目を返す */
699
+ suspend fun castUntilSix(): List<Int> =
700
+ infiniteDieCasts().transformWhile { value ->
701
+ emit(value)
702
+ value != 6
703
+ }.toList()
704
+ ```
705
+
706
+ ```kotlin
707
+ flowOf(1, 2, 3).map { it * 2 }.toList() // [2, 4, 6]
708
+ flowOf(1, 2, 3).zip(flowOf("a", "b", "c")) { n, s -> "$n$s" }.toList() // [1a, 2b, 3c]
709
+ flowOf(1, 2, 3).scan(0) { acc, n -> acc + n }.toList() // [0, 1, 3, 6]
710
+ ```
711
+
712
+ 窓を使ったトレンド検出は、純粋関数として切り出す。
713
+
714
+ ```kotlin
715
+ /** 連続して上昇していれば true */
716
+ fun trending(rates: List<BigDecimal>): Boolean =
717
+ rates.size > 1 && rates.zipWithNext().all { (previous, rate) -> rate > previous }
718
+ ```
719
+
720
+ ---
721
+
722
+ ## 5. 並行処理
723
+
724
+ ### 5.1 共有状態はアトミックに更新する
725
+
726
+ 複数のコルーチンから共有状態を更新するときは、読み取りと書き込みを分けずに `update` で一度に行う。
727
+
728
+ ```kotlin
729
+ /** Atomic に保存されたチェックイン数をアトミックに更新する */
730
+ fun storeCheckIn(storedCheckIns: Atomic<Map<City, Int>>, city: City) {
731
+ storedCheckIns.update { current -> updateCheckIns(current, city) }
732
+ }
733
+ ```
734
+
735
+ ### 5.2 構造化並行性
736
+
737
+ `coroutineScope` の中で起動した `async` は、スコープが終わるまでに必ず完了するか、キャンセルされる。1 つが失敗すれば、兄弟のコルーチンもキャンセルされる。
738
+
739
+ ```kotlin
740
+ /** 2 つのサイコロを並行して振り、合計を返す */
741
+ suspend fun castTheDieTwice(castTheDie: suspend () -> Int): Int = coroutineScope {
742
+ val first = async { castTheDie() }
743
+ val second = async { castTheDie() }
744
+ first.await() + second.await()
745
+ }
746
+ ```
747
+
748
+ ```kotlin
749
+ /** 1 つずつ順番に実行して合計する */
750
+ suspend fun sumSequential(ios: List<suspend () -> Int>): Int =
751
+ ios.sumOf { io -> io() }
752
+
753
+ /** すべてを並行に実行して合計する */
754
+ suspend fun sumConcurrent(ios: List<suspend () -> Int>): Int = coroutineScope {
755
+ ios.map { io -> async { io() } }.awaitAll().sum()
756
+ }
757
+ ```
758
+
759
+ `runTest` の仮想時間を使うと、並行にすることで処理時間が「合計」から「最も遅い処理」に縮むことを、待たずにテストで確かめられる。
760
+
761
+ ```kotlin
762
+ test("sumSequential は処理時間の合計だけかかる") {
763
+ runTest {
764
+ val ios = listOf(delayed(100, 1), delayed(100, 2), delayed(100, 3))
765
+ sumSequential(ios) shouldBe 6
766
+ currentTime shouldBe 300
767
+ }
768
+ }
769
+ test("sumConcurrent は最も遅い処理の時間だけで終わる") {
770
+ runTest {
771
+ val ios = listOf(delayed(100, 1), delayed(200, 2), delayed(100, 3))
772
+ sumConcurrent(ios) shouldBe 6
773
+ currentTime shouldBe 200
774
+ }
775
+ }
776
+ ```
777
+
778
+ ### 5.3 parMap と parZip
779
+
780
+ Arrow の `parMap` はリストの各要素を並列に処理し、同時実行数も制限できる。`parZip` は種類の異なる複数の処理を並列に実行して組み合わせる。
781
+
782
+ ```kotlin
783
+ /** 同時実行数を制限して並列に取得する */
784
+ suspend fun <A, B> fetchAllLimited(
785
+ ids: List<A>,
786
+ concurrency: Int,
787
+ fetch: suspend (A) -> B,
788
+ ): List<B> = ids.parMap(concurrency = concurrency) { id -> fetch(id) }
789
+ ```
790
+
791
+ ```kotlin
792
+ data class Profile(val name: String, val score: Int)
793
+
794
+ /**
795
+ * 名前とスコアを並列に取得して Profile に組み立てる。
796
+ * context を省略すると呼び出し元の Dispatcher を引き継ぐ。
797
+ */
798
+ suspend fun fetchProfile(
799
+ fetchName: suspend () -> String,
800
+ fetchScore: suspend () -> Int,
801
+ context: CoroutineContext = EmptyCoroutineContext,
802
+ ): Profile = parZip(
803
+ context,
804
+ { fetchName() },
805
+ { fetchScore() },
806
+ ) { name, score -> Profile(name, score) }
807
+ ```
808
+
809
+ ### 5.4 競争・タイムアウト・キャンセル
810
+
811
+ | 目的 | 道具 |
812
+ | --- | --- |
813
+ | 先に終わったほうを採用する | `raceN`(結果は `Either` で返る) |
814
+ | 時間で打ち切る | `withTimeout` |
815
+ | バックグラウンドで実行し、後で止める | `Job` と `cancel` |
816
+ | 処理の性質に応じた実行場所 | `Dispatchers.Default`(CPU)、`Dispatchers.IO`(ブロッキング I/O) |
817
+
818
+ ```kotlin
819
+ test("raceN は先に完了した側を Either で返す") {
820
+ runTest {
821
+ val result = raceN(EmptyCoroutineContext, { delay(200); "slow" }, { delay(50); 42 })
822
+ result shouldBe Either.Right(42)
823
+ currentTime shouldBe 50
824
+ }
825
+ }
826
+ ```
827
+
828
+ 複数の共有状態をまとめて更新し、全体を一貫させたいときは、Arrow の STM を使う。
829
+
830
+ ---
831
+
832
+ ## 6. 代数構造の実用
833
+
834
+ モノイド・ファンクタ・モナド・アプリカティブ・プロファンクタは、これまでに使ってきた `fold`、`map`、`flatMap`、`zip` の共通のパターンに付けた名前である。数学の用語を覚えることが目的ではなく、「この型にこの操作が使える」と見抜いて、合成の道具を選べるようにするために使う。
835
+
836
+ Zettai 版では、5 つの構造がすべて現れた。Kotlin では、標準ライブラリと Arrow が与えるものと、自作したものがある。
837
+
838
+ | 構造 | 章 | Kotlin |
839
+ | --- | --- | --- |
840
+ | モノイド | 5 | 自作(標準の `fold` を使う) |
841
+ | ファンクタ | 7・8 | 標準にある(`List.map`、`Either.map`) |
842
+ | モナド | 9 | Arrow にある(`Either.flatMap`、`either { }` と `bind()`) |
843
+ | アプリカティブ | 11 | Arrow にある(`zipOrAccumulate`)。入力の検証では `zipAll` を自作 |
844
+ | プロファンクタ | 12 | 自作(`Encoder` と `Decoder`) |
845
+
846
+ ### 6.1 モノイドでイベントを畳み込む
847
+
848
+ イベントの列を、標準の `fold` で状態へ畳み込む。`fold` は初期値から始め、左から順にイベントを現在の状態へ取り込む。
849
+
850
+ ```kotlin
851
+ fun List<ToDoListEvent>.replayFrom(from: ToDoListState): ToDoListState =
852
+ fold(from) { state, event -> state.apply(event) }
853
+ ```
854
+
855
+ イベントを固定すると、「状態を受け取り状態を返す関数」になる。この関数を値として扱い、合成(`andThen`)と単位元(`identityTransition`)を用意する。
856
+
857
+ ```kotlin
858
+ typealias StateTransition = (ToDoListState) -> ToDoListState
859
+ ```
860
+
861
+ ```kotlin
862
+ fun ToDoListEvent.asTransition(): StateTransition =
863
+ { state -> state.apply(this) }
864
+ ```
865
+
866
+ 二つの状態変換は順番につなげられる。
867
+
868
+ ```kotlin
869
+ infix fun StateTransition.andThen(next: StateTransition): StateTransition =
870
+ { state -> next(this(state)) }
871
+ ```
872
+
873
+ 何もしない変換もある。
874
+
875
+ ```kotlin
876
+ val identityTransition: StateTransition = { it }
877
+ ```
878
+
879
+ イベント列を変換へ畳み込むとき、この単位元を初期値にする。
880
+
881
+ ```kotlin
882
+ fun List<ToDoListEvent>.asTransition(): StateTransition =
883
+ fold(identityTransition) { accumulated, event ->
884
+ accumulated andThen event.asTransition()
885
+ }
886
+ ```
887
+
888
+ この構造には二つの法則がある。
889
+
890
+ - 結合律: `(a andThen b) andThen c` と `a andThen (b andThen c)` の結果は同じである。
891
+ - 単位元: identity を左または右につないでも結果は変わらない。
892
+
893
+ 状態変換、合成、単位元がそろい、法則が成り立つ構造をモノイドと呼ぶ。法則があると、長いイベント列を分割して別々に畳み込み、後で合成しても、結果が変わらない。
894
+
895
+ ### 6.2 ファンクタとモナドは標準と Arrow にある
896
+
897
+ `List.map` と `Either.map` はファンクタである。読み出しが失敗しうるようになると、クエリの結果は `Either<ZettaiError, List<ToDoListRow>>` になり、`map` が二重になる。それでも、恒等則と合成則が成り立つ。
898
+
899
+ ```kotlin
900
+ "クエリ結果の map は恒等則を満たす" {
901
+ checkAll(results) { result ->
902
+ result.map { it } shouldBe result
903
+ }
904
+ }
905
+ ```
906
+
907
+ ```kotlin
908
+ "クエリ結果の map は合成則を満たす" {
909
+ checkAll(results, rowToInt, intToText) { result, f, g ->
910
+ result.map { rows -> rows.map(f) }.map { values -> values.map(g) } shouldBe
911
+ result.map { rows -> rows.map { g(f(it)) } }
912
+ }
913
+ }
914
+ ```
915
+
916
+ `map` に `Either` を返す関数を渡すと、`Either` が二重になる。読み出して、判断して、保存する操作は、それぞれ失敗しうるので、内側を開けて繋ぐ `flatMap` が要る。Arrow の `either { }` と `bind()` は、この `flatMap` の連鎖と同じ結果になり、上から順に読める形で書ける。
917
+
918
+ ```kotlin
919
+ execute = { command ->
920
+ either {
921
+ val state = readAggregate(command.user, command.listName).bind().replayFrom()
922
+ val events = handle(command, state).bind()
923
+ persist(events).bind()
924
+ events
925
+ }
926
+ },
927
+ ```
928
+
929
+ 読み出しの失敗、コマンドの拒否、保存の失敗のどれが起きても、そこで打ち切って同じ `Left` を返す。
930
+
931
+ `flatMap` が予測可能に繋がるには、3 つの性質が必要である。`pure(a)` は値 `a` を成功として包む操作で、`f` と `g` は `Either` を返す関数である。
932
+
933
+ 1. 左恒等則: `pure(a).flatMap(f)` は `f(a)` と同じ
934
+ 2. 右恒等則: `m.flatMap(pure)` は `m` と同じ
935
+ 3. 結合律: `m.flatMap(f).flatMap(g)` は `m.flatMap { x -> f(x).flatMap(g) }` と同じ
936
+
937
+ 法則は、モノイドやファンクタと同じように、プロパティテストで確かめる。
938
+
939
+ ```kotlin
940
+ "左恒等則:pure(a).flatMap(f) は f(a) と同じ" {
941
+ checkAll(values, steps) { a, f ->
942
+ pure(a).flatMap(f) shouldBe f(a)
943
+ }
944
+ }
945
+ ```
946
+
947
+ ```kotlin
948
+ "結合律:flatMap を続けても、中にまとめても同じ" {
949
+ checkAll(results, steps, steps) { m, f, g ->
950
+ m.flatMap(f).flatMap(g) shouldBe m.flatMap { x -> f(x).flatMap(g) }
951
+ }
952
+ }
953
+ ```
954
+
955
+ `bind()` の連鎖が `flatMap` と同じ結果になることも、同じ方法で確かめる。
956
+
957
+ ```kotlin
958
+ // Hub の `either { ... bind() ... }` は、flatMap の連鎖を上から読める形で書いたもの。
959
+ "Raise の bind の連鎖は flatMap と同じ結果になる" {
960
+ checkAll(results, steps, steps) { m, f, g ->
961
+ val withBind: Either<ZettaiError, Int> = either { g(f(m.bind()).bind()).bind() }
962
+
963
+ withBind shouldBe m.flatMap(f).flatMap(g)
964
+ }
965
+ }
966
+ ```
967
+
968
+ この 3 つの性質を満たす `flatMap` と `pure` を持つ構造が、モナドである。
969
+
970
+ ### 6.3 アプリカティブでバリデーションを合成する
971
+
972
+ `either { }` のモナドは、最初のエラーで止まる。独立した複数の検証をすべて走らせ、エラーを全部集めるには、アプリカティブを使う。Arrow の `zipOrAccumulate` は 3.4 節のとおりである。入力の検証では、検証結果の合成に `zipAll` を使う。誤りの側は `NonEmptyList` で、失敗しているなら誤りは必ず 1 つ以上あることを型が保証する。シグネチャは次のとおりである。
973
+
974
+ ```kotlin
975
+ fun <P, A, B, R> zipAll(
976
+ first: Either<NonEmptyList<P>, A>,
977
+ second: Either<NonEmptyList<P>, B>,
978
+ transform: (A, B) -> R,
979
+ ): Either<NonEmptyList<P>, R>
980
+ ```
981
+
982
+ 1 つの名前に対する 3 つの規則は、この合成で 1 つにする。
983
+
984
+ ```kotlin
985
+ fun validateName(field: InputField, raw: String): Either<NonEmptyList<FieldProblem>, String> =
986
+ zipAll(notBlank(field, raw), withinLength(field, raw), noForbiddenCharacter(field, raw)) { _, _, _ -> raw }
987
+ ```
988
+
989
+ 検証が済んだ入力だけがドメインのコマンドになる。境界で生の文字列を検証済みの型に変え、内側では検証を繰り返さない。
990
+
991
+ ### 6.4 プロファンクタで入力と出力の両方を変換する
992
+
993
+ JSON の変換のように、書き込みと読み込みの両方向を持つ型は、入力側を `contramap`、出力側を `map` で変換する。2 つを一度に行うのが `dimap` である。
994
+
995
+ ```kotlin
996
+ data class Encoder<A, B>(val run: (A) -> B) {
997
+ fun <C, D> dimap(contramap: (C) -> A, map: (B) -> D): Encoder<C, D> =
998
+ Encoder { map(run(contramap(it))) }
999
+ }
1000
+ ```
1001
+
1002
+ 読む側の `Decoder` も、同じ形である。
1003
+
1004
+ ```kotlin
1005
+ data class Decoder<B, A>(val run: (B) -> Either<ConversionFailure, A>) {
1006
+ fun <C, D> dimap(contramap: (C) -> B, map: (A) -> D): Decoder<C, D> =
1007
+ Decoder { run(contramap(it)).map(map) }
1008
+ }
1009
+ ```
1010
+
1011
+ どちらも、プロファンクタの形をしている。反変は「入力を受け取る前に、別の型から写す」こと、共変は「出力を返したあとで、別の型へ写す」ことである。求められる法則は次の 2 つである。
1012
+
1013
+ - `dimap id id = id`
1014
+ - `dimap (f ∘ g) (h ∘ k) = dimap g h ∘ dimap f k`
1015
+
1016
+ 法則はプロパティテストで確かめる。プロファンクタという名前は、見つけたものに名前を付けただけで、名前を付けても実装が賢くなるわけではない。法則が通ることだけが、名前の根拠である。
1017
+
1018
+ 2 つを束ねた `Converter` には、入力と出力を同時に写す一般の `dimap` を持たせなかった。行きと帰りで、逆向きの関数が要るためである。持たせたのは、繋ぐ `andThen` である。
1019
+
1020
+ ```kotlin
1021
+ data class Converter<A, B>(val encoder: Encoder<A, B>, val decoder: Decoder<B, A>) {
1022
+ fun <C> andThen(next: Converter<B, C>): Converter<A, C> =
1023
+ Converter(
1024
+ encoder = encoder.dimap({ it }, next.encoder.run),
1025
+ decoder = Decoder { c -> next.decoder.run(c).flatMap(decoder.run) },
1026
+ )
1027
+ }
1028
+ ```
1029
+
1030
+ ### 6.5 どの構造を選ぶか: 判断の手順
1031
+
1032
+ 5 つの構造を「いつ選ぶか」で整理する。
1033
+
1034
+ | 構造 | 持つもの | 選ぶとき | 連載の章 |
1035
+ | --- | --- | --- | --- |
1036
+ | モノイド | 合成と単位元 | 同じ型のものを畳み込みたい | 5 |
1037
+ | ファンクタ | `map` | 箱の中身だけを変えたい | 7・8 |
1038
+ | アプリカティブ | `map` と `combine` | 独立な計算を合わせ、失敗を全部集めたい | 11 |
1039
+ | モナド | `map` と `flatMap` と `pure` | 前の結果を使って次を決めたい | 9・10 |
1040
+ | プロファンクタ | `map` と `contramap` | 入力と出力の両方を変換したい | 12 |
1041
+
1042
+ 実際には、次の順で考えると決まる。
1043
+
1044
+ ```mermaid
1045
+ flowchart TD
1046
+ Q1{入力と出力の両方を<br>変換する?} -->|はい| P[プロファンクタ<br>map と contramap]
1047
+ Q1 -->|いいえ| Q2{前の結果を使って<br>次を決める?}
1048
+ Q2 -->|はい| M[モナド<br>flatMap]
1049
+ Q2 -->|いいえ| Q3{複数の結果を<br>合わせる?}
1050
+ Q3 -->|はい| Q4{失敗を全部<br>集めたい?}
1051
+ Q4 -->|はい| A[アプリカティブ<br>combine]
1052
+ Q4 -->|いいえ| M2[モナドでもよい]
1053
+ Q3 -->|いいえ| Q5{同じ型を<br>畳み込む?}
1054
+ Q5 -->|はい| MO[モノイド]
1055
+ Q5 -->|いいえ| F[ファンクタ<br>map]
1056
+ ```
1057
+
1058
+ 上から順に「いいえ」なら下へ進む。強い構造から確かめて、要らなければ弱いものにする。
1059
+
1060
+ 「強い」とは「できることが多い」という意味で、「偉い」という意味ではない。`map` で足りるなら `map` を使う。できることを増やすと、できなくなることがある。モナドは「前の結果を使う」能力を得た代わりに、「失敗を集める」能力を失った。必要な強さを選ぶ。
1061
+
1062
+ ---
1063
+
1064
+ ## 7. アーキテクチャ: 関数で組み立てる
1065
+
1066
+ ### 7.1 薄い縦串から始める
1067
+
1068
+ 最初にウォーキングスケルトン(HTTP からドメイン、表示までを貫く最小の経路)を作り、外側の受入テストから内側へ進める。http4k の `HttpHandler` は概念上 `(Request) -> Response` の関数なので、サーバーを起動せずにテストできる。
1069
+
1070
+ ```kotlin
1071
+ "GET /todo/{user}/{list} はリストを HTML で返す" {
1072
+ val response = app(Request(GET, "/todo/uberto/book"))
1073
+
1074
+ response.status shouldBe Status.OK
1075
+ response.bodyString() shouldContain "<h2>book</h2>"
1076
+ response.bodyString() shouldContain "<li>write chapter</li>"
1077
+ response.bodyString() shouldContain "<li>publish book</li>"
1078
+ }
1079
+ ```
1080
+
1081
+ ### 7.2 依存は関数の型で表す
1082
+
1083
+ 外部とのやり取りをインターフェースのクラス階層ではなく、関数の型(`typealias`)で表す。テストでは同じ型の関数に差し替えるだけでよい。
1084
+
1085
+ ```kotlin
1086
+ typealias ToDoListFetcher = suspend (User, ListName) -> ToDoList?
1087
+
1088
+ data class ToDoListHub(
1089
+ val getList: ToDoListFetcher,
1090
+ )
1091
+
1092
+ fun todoListHub(fetchList: ToDoListFetcher): ToDoListHub =
1093
+ ToDoListHub(getList = fetchList)
1094
+ ```
1095
+
1096
+ ```kotlin
1097
+ val hub = todoListHub { actualUser, actualName ->
1098
+ if (actualUser == user && actualName == listName) expected else null
1099
+ }
1100
+ ```
1101
+
1102
+ 同じ受入テストを、ドメイン層と HTTP 層の両方に対して流せるようにする方法もある。操作を関数の束にして、層ごとの実装を並べる。
1103
+
1104
+ ```kotlin
1105
+ data class ZettaiActions(
1106
+ val name: String,
1107
+ val setUp: (User, ToDoList) -> Unit,
1108
+ val getToDoList: (User, ListName) -> List<ToDoItem>?,
1109
+ )
1110
+ ```
1111
+
1112
+ ### 7.3 状態をイベントの列として扱う(イベントソーシング)
1113
+
1114
+ 現在の状態を直接更新せず、起きた事実(イベント)を追記し、状態はイベントの再生で導く。状態の更新は、`when` で網羅した純粋関数になる。
1115
+
1116
+ ```kotlin
1117
+ fun ToDoListState.apply(event: ToDoListEvent): ToDoListState {
1118
+ return when (event) {
1119
+ is ListCreated -> put(
1120
+ event.user,
1121
+ event.listName,
1122
+ ToDoList(event.listName, emptyList()),
1123
+ )
1124
+
1125
+ is ItemAdded -> {
1126
+ val current = listFor(event.user, event.listName) ?: return this
1127
+ put(
1128
+ event.user,
1129
+ event.listName,
1130
+ current.copy(items = current.items + event.item),
1131
+ )
1132
+ }
1133
+ }
1134
+ }
1135
+ ```
1136
+
1137
+ `copy`、`Map.plus`、`List.plus` は元の値を書き換えない。イベントの種類が増えれば、`when` の分岐も増える(網羅性はコンパイラが検査する)。列全体の再生は、1 件の適用を `fold` で繰り返すだけである(6.1 節)。
1138
+
1139
+ ### 7.4 コマンドを検証し、イベントを生成する
1140
+
1141
+ コマンド(意図)を、現在の状態に照らして検証し、成功すればイベントを返す。失敗は `Either` で表す。
1142
+
1143
+ ```kotlin
1144
+ sealed interface ToDoListCommand {
1145
+ val user: User
1146
+ val listName: ListName
1147
+ }
1148
+
1149
+ data class CreateToDoList(
1150
+ override val user: User,
1151
+ override val listName: ListName,
1152
+ ) : ToDoListCommand
1153
+ ```
1154
+
1155
+ 業務ルール(状態遷移の許可)は、データとして宣言する。
1156
+
1157
+ ```kotlin
1158
+ private val allowedTransitions = mapOf(
1159
+ ToDoStatus.Todo to setOf(ToDoStatus.InProgress, ToDoStatus.Done),
1160
+ ToDoStatus.InProgress to setOf(ToDoStatus.Done, ToDoStatus.Blocked),
1161
+ ToDoStatus.Done to emptySet(),
1162
+ ToDoStatus.Blocked to setOf(ToDoStatus.InProgress),
1163
+ )
1164
+
1165
+ fun ToDoStatus.canTransitionTo(next: ToDoStatus): Boolean =
1166
+ next in allowedTransitions.getValue(this)
1167
+ ```
1168
+
1169
+ 名前の変更のように、1 つのコマンドが複数のイベントを生むこともある。
1170
+
1171
+ ```kotlin
1172
+ is RenameToDoList -> {
1173
+ val list = ensureNotNull(state.listFor(command.user, command.listName)) {
1174
+ ListNotFound(command.user, command.listName)
1175
+ }
1176
+ ensure(state.listFor(command.user, command.newName) == null) {
1177
+ ListAlreadyExists(command.user, command.newName)
1178
+ }
1179
+ listOf(
1180
+ ListRenamed(command.user, command.listName, command.newName),
1181
+ ListCreated(command.user, command.newName),
1182
+ ) + list.items.map { ItemAdded(command.user, command.newName, it) }
1183
+ }
1184
+ ```
1185
+
1186
+ ### 7.5 コマンドと問い合わせの分離(CQRS)
1187
+
1188
+ 書き込み(コマンド → イベント)と読み取り(イベント → 射影)を分ける。読み取り側は、イベントの列を畳み込む純粋関数として書ける。
1189
+
1190
+ ```kotlin
1191
+ fun projectFrom(user: User, events: List<ToDoListEvent>): List<ToDoListRow> =
1192
+ events
1193
+ .filter { it.user == user }
1194
+ .fold(emptyMap<ListName, List<ToDoItem>>()) { lists, event -> lists.step(event) }
1195
+ .map { (listName, items) ->
1196
+ ToDoListRow(
1197
+ listName = listName,
1198
+ itemCount = items.size,
1199
+ doneCount = items.count { it.status == ToDoStatus.Done },
1200
+ )
1201
+ }
1202
+ ```
1203
+
1204
+ ### 7.6 永続化とトランザクション境界
1205
+
1206
+ 永続化の失敗は例外のまま漏らさず、境界で `Either` に変換する。ブロッキング I/O は `Dispatchers.IO` に閉じ込める。
1207
+
1208
+ ```kotlin
1209
+ private suspend fun <A> blocking(block: () -> A): Either<PersistenceFailure, A> =
1210
+ withContext(Dispatchers.IO) { Either.catch(block) }
1211
+ .mapLeft { PersistenceFailure("${it::class.simpleName}: ${it.message}") }
1212
+ ```
1213
+
1214
+ ストアも関数の束として定義すると、PostgreSQL 版とインメモリ版を差し替えられる。
1215
+
1216
+ ```kotlin
1217
+ fun postgresEventStore(connect: () -> Connection): EventStore =
1218
+ EventStore(
1219
+ readAggregate = { user, listName ->
1220
+ query(connect, EventTable.SELECT_BY_ENTITY, entityId(user, listName))
1221
+ },
1222
+ readUser = { user ->
1223
+ query(connect, EventTable.SELECT_BY_USER, userPrefix(user))
1224
+ },
1225
+ persist = { events -> append(connect, events) },
1226
+ )
1227
+ ```
1228
+
1229
+ 複数のストリームをまたぐコマンドは、「読む → 検証する → 追記する」を 1 つのトランザクションの中で行う。読んだときのバージョンを追記時に渡して、競合を検出する。
1230
+
1231
+ ```kotlin
1232
+ execute = { command ->
1233
+ store.transaction { tx ->
1234
+ either {
1235
+ val read = command.streams.associateWith { store.readStream(tx, it).bind() }
1236
+ val state = read.values.flatMap { it.events }.replayFrom()
1237
+ val events = handle(command, state).bind()
1238
+ store.append(tx, read.mapValues { it.value.version }, events).bind()
1239
+ events
1240
+ }
1241
+ }
1242
+ },
1243
+ ```
1244
+
1245
+ ### 7.7 監視は合成で足す
1246
+
1247
+ ログや計測は、ビジネスのコードを変えずに、関数をラップして足す。
1248
+
1249
+ ```kotlin
1250
+ fun ToDoListHub.logged(log: (String) -> Unit, now: () -> Instant): ToDoListHub {
1251
+ val inner = this
1252
+ return ToDoListHub(
1253
+ getList = { user, listName ->
1254
+ observe(log, now, "query", "getList", user, listName) { inner.getList(user, listName) }
1255
+ },
1256
+ listRows = { user ->
1257
+ observe(log, now, "query", "listRows", user, null) { inner.listRows(user) }
1258
+ },
1259
+ execute = { command ->
1260
+ observe(
1261
+ log,
1262
+ now,
1263
+ "command",
1264
+ command::class.simpleName ?: "Command",
1265
+ command.user,
1266
+ command.listName,
1267
+ events = { it.size },
1268
+ ) { inner.execute(command) }
1269
+ },
1270
+ )
1271
+ }
1272
+ ```
1273
+
1274
+ 構造化されたログの 1 件は、データクラスで表す。
1275
+
1276
+ ```kotlin
1277
+ data class LogRecord(
1278
+ val at: Instant,
1279
+ val kind: String,
1280
+ val name: String,
1281
+ val outcome: String,
1282
+ val user: String? = null,
1283
+ val list: String? = null,
1284
+ val durationMs: Long,
1285
+ val streams: Int? = null,
1286
+ val events: Int? = null,
1287
+ val path: String? = null,
1288
+ val status: Int? = null,
1289
+ )
1290
+ ```
1291
+
1292
+ ### 7.8 全体の形
1293
+
1294
+ 出典の最終章がたどり着いた設計は、次の形である。
1295
+
1296
+ ```mermaid
1297
+ flowchart LR
1298
+ A[HTTP<br>Request → Response] --> B[入力の検証<br>アプリカティブ]
1299
+ B --> C[コマンド]
1300
+ C --> D[handle<br>状態 + コマンド → Either イベント列]
1301
+ D --> E[イベントストア<br>トランザクション]
1302
+ E --> F[射影<br>fold]
1303
+ F --> G[HTML 表示]
1304
+ ```
1305
+
1306
+ 核になる純粋関数(検証、`handle`、`apply`、射影)を中心に置き、副作用(HTTP、データベース、ログ)は外側の薄い層に寄せる。
1307
+
1308
+ ---
1309
+
1310
+ ## 8. テスト戦略
1311
+
1312
+ 純粋関数は、入力を与えて出力を確かめるだけでテストできる。副作用は関数として外から渡し、テスト用に差し替える。
1313
+
1314
+ ### 8.1 失敗を型で返す
1315
+
1316
+ 「良いガイドが見つからなかった」ことも、失敗の理由を持った値で返す。
1317
+
1318
+ ```kotlin
1319
+ /** 検索レポート(良いガイドが見つからなかった理由を報告する) */
1320
+ data class SearchReport(val badGuides: List<Guide>, val problems: List<String>)
1321
+ ```
1322
+
1323
+ ### 8.2 テスト用スタブ
1324
+
1325
+ 外部への依存をインターフェースで受け取り、テスト用の実装は関数で差し替えられるようにする。失敗のシミュレーションも関数で書ける。
1326
+
1327
+ ```kotlin
1328
+ class TestDataAccess(
1329
+ private val attractions: suspend (name: String) -> List<Attraction> = { emptyList() },
1330
+ private val artists: suspend (locationId: LocationId) -> List<Artist> = { emptyList() },
1331
+ private val movies: suspend (locationId: LocationId) -> List<Movie> = { emptyList() },
1332
+ ) : DataAccess {
1333
+ ```
1334
+
1335
+ ```kotlin
1336
+ test("良いガイドが見つからなければ、悪いガイドを含む SearchReport を返す") {
1337
+ val dataAccess = TestDataAccess.of(attractions = listOf(yellowstone))
1338
+
1339
+ travelGuideV3(dataAccess, "") shouldBe
1340
+ SearchReport(badGuides = listOf(Guide(yellowstone, emptyList())), problems = emptyList()).left()
1341
+ }
1342
+ ```
1343
+
1344
+ ### 8.3 プロパティベーステスト
1345
+
1346
+ 具体的な値の例ではなく、「どんな入力でも成り立つ性質」をテストする。Kotest の `checkAll` と `Arb` で入力を生成する。
1347
+
1348
+ ```kotlin
1349
+ val nonNegativeInt: Arb<Int> = Arb.int(0..Int.MAX_VALUE)
1350
+
1351
+ val identifier: Arb<String> = Arb.string(1..10, Codepoint.alphanumeric())
1352
+
1353
+ val randomArtist: Arb<Artist> = arbitrary { Artist(identifier.bind(), nonNegativeInt.bind()) }
1354
+
1355
+ val randomMovie: Arb<Movie> = Arb.bind(identifier, nonNegativeInt) { name, boxOffice -> Movie(name, boxOffice) }
1356
+ ```
1357
+
1358
+ ```kotlin
1359
+ test("スコアはアトラクションの名前と説明の文字列に依存しない") {
1360
+ checkAll(Arb.string(), Arb.string()) { name, description ->
1361
+ val guide = Guide(
1362
+ Attraction(name, description, wyoming),
1363
+ listOf(Movie("The Hateful Eight", 155_760_117), Movie("Heaven's Gate", 3_484_331)),
1364
+ )
1365
+ guideScore(guide) shouldBe 65
1366
+ }
1367
+ }
1368
+ ```
1369
+
1370
+ 性質のテストは、バグを見つける力も強い。次のテストは、`Int` のまま合計するとオーバーフローすることを示している。
1371
+
1372
+ ```kotlin
1373
+ test("Int のまま合計するとオーバーフローするので Long で合計する") {
1374
+ val followers = listOf(Int.MAX_VALUE, Int.MAX_VALUE)
1375
+ followers.sum() shouldBe -2
1376
+ followers.sumOf { it.toLong() } shouldBe 4_294_967_294L
1377
+ }
1378
+ ```
1379
+
1380
+ 参照透過性そのものも、性質として書ける。
1381
+
1382
+ ```kotlin
1383
+ test("guideScore は同じ入力に対して常に同じ出力を返す(参照透過性)") {
1384
+ forAll(randomGuide) { guide -> guideScore(guide) == guideScore(guide) }
1385
+ }
1386
+ ```
1387
+
1388
+ | テストの種類 | 確かめること | 道具 |
1389
+ | --- | --- | --- |
1390
+ | 例によるテスト | 代表的な入力での振る舞い | Kotest の `shouldBe` |
1391
+ | スタブを使ったテスト | 副作用の失敗や組み合わせでの振る舞い | `TestDataAccess` のような関数で差し替える実装 |
1392
+ | プロパティベーステスト | どんな入力でも成り立つ性質 | `checkAll`、`forAll`、`Arb` |
1393
+ | 仮想時間のテスト | 並行処理の所要時間 | `runTest` と `currentTime` |
1394
+ | 結合テスト | 実際のデータベースとの接続 | Testcontainers の PostgreSQL |
1395
+
1396
+ ---
1397
+
1398
+ ## 9. 静的解析とルール設定
1399
+
1400
+ 関数型の規約(`var` を避ける、可変コレクションを共有しない、例外を握りつぶさない)は、レビューだけに頼ると漏れる。静的解析で機械的に守らせる。ただし、ツールの選択を先に間違えないことが重要である。
1401
+
1402
+ ### 9.1 ツールの選び方
1403
+
1404
+ | ツール | 対象 | 役割 | Kotlin への適用 |
1405
+ | --- | --- | --- | --- |
1406
+ | PMD | Java、Kotlin(PMD 7.0 から) | バグの兆候、設計、コードスタイルの検査。XPath でカスタムルールを書ける | 標準の Kotlin ルールは少数(後述)。強みは XPath によるカスタムルール |
1407
+ | detekt | Kotlin | 複雑度、潜在的なバグ、スタイルの検査 | 標準ルールが豊富。中心になる静的解析 |
1408
+ | ktlint | Kotlin | 整形(フォーマット) | 書式の統一。後述の副作用に注意 |
1409
+ | Checkstyle / SpotBugs | Java | 書式、バグパターン | Kotlin には適用できない |
1410
+
1411
+ PMD は 7.0 から Kotlin に対応している([PMD の Kotlin 対応](https://pmd.github.io/pmd/pmd_languages_kotlin.html))。ただし、PMD 7.29.0 の時点で標準の Kotlin ルールは次の 4 つだけである([Kotlin のルール一覧](https://pmd.github.io/pmd/pmd_rules_kotlin.html)で確認)。
1412
+
1413
+ | カテゴリ | ルール | 内容 |
1414
+ | --- | --- | --- |
1415
+ | Best Practices | `FunctionNameTooShort` | 短すぎる関数名 |
1416
+ | Best Practices | `LocalVariableShadowsParameter` | 引数と同名のローカル変数 |
1417
+ | Best Practices | `LooseCoupling` | 実装型(`HashMap` など)への依存 |
1418
+ | Error Prone | `OverrideBothEqualsAndHashcode` | `equals` と `hashCode` の片方だけの上書き |
1419
+
1420
+ そのため、Kotlin では標準ルールの数と種類が多い detekt を中心にしつつ、**関数型の規約を表すカスタムルールは、XPath で書ける PMD でも実装できる**(9.6 節)。PMD が Java にも使えることは、Kotlin と Java のソースが混在するプロジェクトで、1 つのツールで両方を検査できるという利点になる。
1421
+
1422
+ ### 9.2 PMD のルール設定(Java)
1423
+
1424
+ 出典の Java プロジェクトで使っている `ruleset.xml` である。Kotlin 向けには、`category/java/...` の代わりに `category/kotlin/bestpractices.xml` などを参照する(9.6 節)。カテゴリ単位で有効にし、プロジェクトに合わないルールを除外し、複雑度のしきい値を明示する。
1425
+
1426
+ ```xml
1427
+ <?xml version="1.0"?>
1428
+ <ruleset name="Custom Rules"
1429
+ xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
1430
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1431
+ xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
1432
+
1433
+ <description>
1434
+ プロジェクト用 PMD ルールセット
1435
+ </description>
1436
+
1437
+ <!-- 基本ルール -->
1438
+ <rule ref="category/java/bestpractices.xml">
1439
+ <exclude name="JUnitTestContainsTooManyAsserts"/>
1440
+ </rule>
1441
+ <rule ref="category/java/errorprone.xml"/>
1442
+ <rule ref="category/java/codestyle.xml">
1443
+ <exclude name="AtLeastOneConstructor"/>
1444
+ <exclude name="OnlyOneReturn"/>
1445
+ <exclude name="LocalVariableCouldBeFinal"/>
1446
+ <exclude name="MethodArgumentCouldBeFinal"/>
1447
+ <exclude name="ShortVariable"/>
1448
+ <exclude name="LongVariable"/>
1449
+ <exclude name="ShortMethodName"/>
1450
+ <exclude name="ShortClassName"/>
1451
+ <exclude name="CommentDefaultAccessModifier"/>
1452
+ </rule>
1453
+
1454
+ <!-- コード複雑度 -->
1455
+ <rule ref="category/java/design.xml/CyclomaticComplexity">
1456
+ <properties>
1457
+ <property name="classReportLevel" value="80"/>
1458
+ <property name="methodReportLevel" value="7"/>
1459
+ </properties>
1460
+ </rule>
1461
+ <rule ref="category/java/design.xml/CognitiveComplexity">
1462
+ <properties>
1463
+ <property name="reportLevel" value="4"/>
1464
+ </properties>
1465
+ </rule>
1466
+ <rule ref="category/java/design.xml/NPathComplexity">
1467
+ <properties>
1468
+ <property name="reportLevel" value="200"/>
1469
+ </properties>
1470
+ </rule>
1471
+ </ruleset>
1472
+ ```
1473
+
1474
+ Gradle では、`pmd` プラグインに設定ファイルを渡す。`ruleSets = []` は、プラグイン既定のルールセットを無効にして、上のファイルだけを使うための指定である。
1475
+
1476
+ ```groovy
1477
+ plugins {
1478
+ id 'pmd'
1479
+ }
1480
+
1481
+ pmd {
1482
+ ruleSetFiles = files('config/pmd/ruleset.xml')
1483
+ ruleSets = []
1484
+ }
1485
+ ```
1486
+
1487
+ 方針は次のとおりである。
1488
+
1489
+ - **カテゴリごとに有効にして、合わないものだけ除外する。** 個別のルールを列挙するより、新しい版で追加されたルールも自動で効く。
1490
+ - **除外には理由がある。** `OnlyOneReturn` は早期 return を禁じるので、ガード節で平らに書く関数型のスタイルと衝突する。`LocalVariableCouldBeFinal` などは、`final` を付ける代わりに `val` で表す Kotlin では意味を持たない(Java 側の規約)。
1491
+ - **しきい値は数値で明示する。** 複雑度はメソッドごとに小さく保つ(出典では循環的複雑度 7、認知的複雑度 4)。小さな関数を合成するスタイルと相性がよい。
1492
+
1493
+ ### 9.3 detekt のルール設定(Kotlin)
1494
+
1495
+ Kotlin では detekt を中心にする。次の設定は、関数型のスタイルを支える代表的なルールの例である。出典のリポジトリには detekt の設定ファイルがなく、ここに書く設定は **出典から転記したものではなく、実際に走らせて確認してもいない**。導入時は、使う detekt のバージョンのルール名と既定値を公式のドキュメントで確認すること。
1496
+
1497
+ ```yaml
1498
+ # detekt.yml(例)
1499
+ build:
1500
+ maxIssues: 0
1501
+
1502
+ complexity:
1503
+ CyclomaticComplexMethod:
1504
+ active: true
1505
+ threshold: 15
1506
+ CognitiveComplexMethod:
1507
+ active: true
1508
+ threshold: 15
1509
+ LongParameterList:
1510
+ active: true
1511
+ functionThreshold: 6
1512
+ ignoreDefaultParameters: true
1513
+
1514
+ exceptions:
1515
+ TooGenericExceptionCaught:
1516
+ active: true
1517
+ SwallowedException:
1518
+ active: true
1519
+
1520
+ potential-bugs:
1521
+ UnsafeCallOnNullableType:
1522
+ active: true
1523
+
1524
+ style:
1525
+ MaxLineLength:
1526
+ active: true
1527
+ maxLineLength: 120
1528
+ ForbiddenMethodCall:
1529
+ active: true
1530
+ methods:
1531
+ - reason: 'コレクションの共有可変状態を避ける。必要なら buildList の内側に閉じ込める'
1532
+ value: 'kotlin.collections.mutableListOf'
1533
+ ```
1534
+
1535
+ 関数型の観点で効くルールは次のとおりである。
1536
+
1537
+ | 規約 | ルール(例) | ねらい |
1538
+ | --- | --- | --- |
1539
+ | 例外を値に置き換える | `TooGenericExceptionCaught`、`SwallowedException` | 例外を握りつぶさず、`Either.catch` などで型にする |
1540
+ | null の安全な扱い | `UnsafeCallOnNullableType` | `!!` を避け、`?:` や `ensureNotNull` を使う |
1541
+ | 可変状態を共有しない | `ForbiddenMethodCall` | 可変コレクションの生成を検出する |
1542
+ | 関数を小さく保つ | `CyclomaticComplexMethod`、`CognitiveComplexMethod`、`LongParameterList` | 小さな関数の合成を促す |
1543
+ | 読みやすさ | `MaxLineLength` | 行長を揃える |
1544
+
1545
+ `var` を禁止する専用のルールは、detekt の標準では確認できていない。`var` の抑制などは、9.6 節のカスタムルールで補う。
1546
+
1547
+ ### 9.4 ktlint を使うときの注意
1548
+
1549
+ ktlint の標準ルールを適用すると、整形の副作用が出ることがある。出典の連載(Zettai Kotlin 版)で実際に導入を試した結果は次のとおりである。
1550
+
1551
+ | 入った整形 | 影響 |
1552
+ | --- | --- |
1553
+ | 引数が 2 つ以上の関数シグネチャを強制的に複数行に分割 | コードが縦長になり、記事のコードブロックが読みにくくなる |
1554
+ | `if (x) a else b` を必ずブロックに展開 | 同上 |
1555
+ | 式の本体を必ず改行してインデント | 同上 |
1556
+ | ファイル名をクラス名に合わせる | 同じパッケージに OO 版と FP 版を並べにくい |
1557
+ | 関数名を camelCase に強制 | バッククォートで書く日本語のテスト名が書けない |
1558
+
1559
+ 下の 2 つは `.editorconfig` でルールを外して回避できる。しかし上の 3 つは ktlint の中核の整形で、外すと導入の意味がほとんど残らない。この連載では、整形を適用した結果、記事のコード例が 40 件、実装とずれた。
1560
+
1561
+ ### 9.5 ツールを入れる前に、守りたいことを決める
1562
+
1563
+ 出典の連載は、静的解析を `check` に組み込まない決定をした(ADR-004)。判断の流れは、ツールの選択の参考になる。
1564
+
1565
+ 1. **実際に起きている問題を数える。** 行長 120 超が 2 件、`import` の順序の乱れが 3 件。タブの混入は 0 件だった。
1566
+ 2. **汎用ツールの副作用を測る。** ktlint は整形が入りすぎて、目的に対して副作用が大きかった。
1567
+ 3. **必要な検査だけを小さく作る。** 行長と `import` の順序だけを検査する小さなスクリプトで足りた。行長の違反は、鍵を作る関数を切り出して直した。汎用ツールの整形より、意味のある切り出しになった。
1568
+ 4. **再検討の条件を事象で決める。** 「開発者が 2 名以上になる」「複雑度や未使用コード、バグの兆候など、自作の検査で拾えない問題が実際に起きる」のいずれかが起きたとき。時期が来たことは、判断の理由にならない。
1569
+
1570
+ | 状況 | 推奨 |
1571
+ | --- | --- |
1572
+ | 複数人の開発、長く保守するプロダクト | detekt を `check` に組み込み、ktlint の整形は `.editorconfig` で調整する |
1573
+ | 1 人の教材・記事のコード | 必要な検査だけを自作する。汎用ツールは副作用を測ってから入れる |
1574
+ | Java の資産が混在する | Java 側は PMD、Checkstyle、SpotBugs。Kotlin 側は detekt。PMD に揃えるなら、Kotlin も PMD のカスタムルールで検査する |
1575
+
1576
+ 静的解析は、検出できる種類の問題にしか効かない。たとえば「記事に載せたコードの実装が存在しなかった」という問題は、静的解析では検出できず、コード例を実装と突き合わせる検査が必要だった。入れるツールは、実際に起きる問題に合わせて選ぶ。
1577
+
1578
+ ### 9.6 関数型ルールのカスタム PMD ルール
1579
+
1580
+ 標準ルールでは表せない関数型の規約は、PMD の XPath ルールで検査する。Kotlin のソースは構文木に変換されるので、木の形に対する条件を XPath で書くだけでよい。Java で実装するプラグインや Gradle のサブプロジェクトは要らず、ルールセットの XML 1 ファイルで完結する。
1581
+
1582
+ > **検証状況**: PMD 7.29.0 のコマンドライン版で実行して確認した(下の「ルールの確認」の期待件数と一致した)。ノード名は PMD の文法定義(`Kotlin.g4` と `KotlinLexer.g4`)と、PMD 自身の Kotlin ルール(`FunctionNameTooShort` と `LooseCoupling`)の XPath から読み取って決めた。ほかの版では、「ルールの確認」の手順で先に確かめること。
1583
+
1584
+ #### ルール一覧
1585
+
1586
+ 各章の内容を、機械で検査できる形のルールにした。章ごとの対応は次のとおりである。
1587
+
1588
+ | 章 | ルール | 検出するもの | 根拠となる規約 |
1589
+ | --- | --- | --- | --- |
1590
+ | 1 基礎 | `NoVarProperty` | クラスやトップレベルの `var` プロパティ(関数内のローカルな `var` は許す) | 状態を持たせず、値を変換する(1.3 節) |
1591
+ | 1 基礎 | `NoVarClassParameter` | コンストラクタの `var`(`data class(var x: Int)` など) | 値は不変にし、`copy` で新しい値を作る(2.1 節、7.3 節) |
1592
+ | 2 関数型スタイル | `NoMutableCollectionInSignature` | 公開関数の引数・戻り値の `MutableList` / `MutableSet` / `MutableMap` / `MutableCollection` | 可変コレクションを共有しない(2.1 節) |
1593
+ | 2 関数型スタイル | `NoMutableCollectionFactory` | `buildList` / `buildSet` / `buildMap` の外での `mutableListOf` や `ArrayList()` などの生成 | 可変操作は `buildList` の内側に閉じ込める(2.1 節) |
1594
+ | 2 関数型スタイル | `NoReduce` | `reduce` / `reduceRight` | 空のコレクションで例外を投げる。`fold` か `reduceOrNull` を使う(2.3 節) |
1595
+ | 3 型で表すエラー | `NoNotNullAssertion` | `!!` | null は `?:` や `ensureNotNull` で扱う(3.1 節) |
1596
+ | 3 型で表すエラー | `NoThrow` | `throw` 式 | 失敗は例外ではなく `Either` で返す(第 3 章) |
1597
+ | 3 型で表すエラー | `NoGenericCatch` | `Exception` / `Throwable` / `RuntimeException` の `catch` | 例外は `Either.catch` で値にする(4.3 節) |
1598
+ | 4 副作用の管理 | `SideEffectInNonSuspendFunction` | `suspend` でない関数の中での、時計・乱数・UUID・標準出力の呼び出し | 隠れた入力と副作用は `suspend` で型に表す(1.2 節、4.2 節) |
1599
+ | 4 副作用の管理 | `NoRunBlocking` | `runBlocking` の呼び出し | 副作用の型付け(`suspend`)を呼び出し側で打ち消さない(4.2 節) |
1600
+ | 5 並行処理 | `NoGlobalScope` | `GlobalScope` の使用 | 構造化並行性の中で起動する(5.2 節) |
1601
+ | 5 並行処理 | `NoRawConcurrency` | `Thread(...)`、`thread { }`、`Executors`、`synchronized`、`@Synchronized` | スレッドやロックではなく、コルーチンと `Atomic` で共有状態を更新する(5.1 節) |
1602
+ | 5 並行処理、8 テスト戦略 | `NoThreadSleep` | `Thread.sleep` | スレッドを塞ぐ。`delay` と、テストでは `runTest` の仮想時間を使う(5.2 節) |
1603
+ | 6 代数構造の実用 | `NoNestedFlatMap` | `flatMap` の中の `flatMap` | `either { }` と `bind()`、`sequence { }` で平らに書く(2.5 節、3.3 節) |
1604
+
1605
+ 第 7 章(アーキテクチャ)は、イベントやコマンドを不変の値で表す規約として `NoVarProperty` と `NoVarClassParameter`、境界の一度きりの `runBlocking` として `NoRunBlocking` が対応する。第 8 章(テスト戦略)は、`Thread.sleep` と `runBlocking` を使わずに `runTest` で時間を扱う規約として `NoThreadSleep` と `NoRunBlocking` が対応する。`sealed interface` の `when` に `else` を書かない、純粋関数の性質をプロパティベーステストで書く、といった規約は、型情報やテストの内容が要るため、構文の検査では表せない。レビューとテストで守る。
1606
+
1607
+ #### ルールセット
1608
+
1609
+ ```xml
1610
+ <?xml version="1.0"?>
1611
+ <ruleset name="FP Kotlin Rules"
1612
+ xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
1613
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1614
+ xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
1615
+ <description>関数型プログラミングガイド(Kotlin 編)に準拠したカスタムルール</description>
1616
+
1617
+ <rule name="NoVarProperty" language="kotlin"
1618
+ message="var プロパティは val にし、状態の書き換えではなく新しい値を作る"
1619
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1620
+ <description>クラスやトップレベルの var プロパティを禁止する。関数内のローカルな var は許す。</description>
1621
+ <priority>3</priority>
1622
+ <properties>
1623
+ <property name="xpath"><value><![CDATA[
1624
+ //PropertyDeclaration[@Mutable = true()]
1625
+ [not(ancestor::FunctionBody or ancestor::AnonymousInitializer or ancestor::SecondaryConstructor)]
1626
+ ]]></value></property>
1627
+ </properties>
1628
+ </rule>
1629
+
1630
+ <rule name="NoNotNullAssertion" language="kotlin"
1631
+ message="!! を使わず、?: や ?.let で null を型として扱う"
1632
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1633
+ <description>非 null 表明(!!)を禁止する。</description>
1634
+ <priority>2</priority>
1635
+ <properties>
1636
+ <property name="xpath"><value><![CDATA[
1637
+ //PostfixUnaryOperator[Excl]
1638
+ ]]></value></property>
1639
+ </properties>
1640
+ </rule>
1641
+
1642
+ <rule name="NoThrow" language="kotlin"
1643
+ message="throw の代わりに Either などの戻り値の型で失敗を表す"
1644
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1645
+ <description>throw 式を禁止する。</description>
1646
+ <priority>3</priority>
1647
+ <properties>
1648
+ <property name="xpath"><value><![CDATA[
1649
+ //JumpExpression[T-THROW]
1650
+ ]]></value></property>
1651
+ </properties>
1652
+ </rule>
1653
+
1654
+ <rule name="NoMutableCollectionInSignature" language="kotlin"
1655
+ message="公開関数の引数と戻り値では読み取り専用のコレクション型を使う"
1656
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1657
+ <description>公開関数のシグネチャに MutableList / MutableSet / MutableMap / MutableCollection を使うことを禁止する。</description>
1658
+ <priority>3</priority>
1659
+ <properties>
1660
+ <property name="xpath"><value><![CDATA[
1661
+ //FunctionDeclaration[not(ancestor::FunctionBody)]
1662
+ [not(pmd-kotlin:modifiers() = ('private', 'protected', 'internal'))]
1663
+ //SimpleUserType[not(ancestor::FunctionBody)]/SimpleIdentifier/T-Identifier
1664
+ [@Text = ('MutableList', 'MutableSet', 'MutableMap', 'MutableCollection')]
1665
+ ]]></value></property>
1666
+ </properties>
1667
+ </rule>
1668
+
1669
+ <rule name="SideEffectInNonSuspendFunction" language="kotlin"
1670
+ message="時計・乱数・スリープ・標準出力などの副作用は suspend 関数の中に置くか、引数で外から受け取る"
1671
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1672
+ <description>suspend でない関数の中で、隠れた入力や副作用を持つ呼び出しをすることを禁止する。</description>
1673
+ <priority>3</priority>
1674
+ <properties>
1675
+ <property name="xpath"><value><![CDATA[
1676
+ //PostfixUnaryExpression
1677
+ [ancestor::FunctionDeclaration[1]]
1678
+ [not(ancestor::FunctionDeclaration[1]/Modifiers//T-SUSPEND)]
1679
+ [
1680
+ (
1681
+ PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = ('println', 'print')]
1682
+ and PostfixUnarySuffix[1]/CallSuffix
1683
+ )
1684
+ or (
1685
+ PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'System']
1686
+ and PostfixUnarySuffix[1]/NavigationSuffix/SimpleIdentifier/T-Identifier
1687
+ [@Text = ('currentTimeMillis', 'nanoTime')]
1688
+ and PostfixUnarySuffix[2]/CallSuffix
1689
+ )
1690
+ or (
1691
+ PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'Random']
1692
+ and PostfixUnarySuffix[1]/NavigationSuffix/SimpleIdentifier/T-Identifier
1693
+ [starts-with(@Text, 'next')]
1694
+ and PostfixUnarySuffix[2]/CallSuffix
1695
+ )
1696
+ or (
1697
+ PrimaryExpression/SimpleIdentifier/T-Identifier
1698
+ [@Text = ('Instant', 'LocalDate', 'LocalDateTime', 'LocalTime', 'ZonedDateTime', 'OffsetDateTime')]
1699
+ and PostfixUnarySuffix[1]/NavigationSuffix/SimpleIdentifier/T-Identifier[@Text = 'now']
1700
+ and PostfixUnarySuffix[2]/CallSuffix
1701
+ )
1702
+ or (
1703
+ PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'UUID']
1704
+ and PostfixUnarySuffix[1]/NavigationSuffix/SimpleIdentifier/T-Identifier[@Text = 'randomUUID']
1705
+ and PostfixUnarySuffix[2]/CallSuffix
1706
+ )
1707
+ ]
1708
+ ]]></value></property>
1709
+ </properties>
1710
+ </rule>
1711
+
1712
+ <rule name="NoRunBlocking" language="kotlin"
1713
+ message="runBlocking で suspend の型付けを打ち消さない。呼び出し側も suspend にするか、境界で一度だけ使う"
1714
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1715
+ <description>runBlocking の呼び出しを禁止する。</description>
1716
+ <priority>3</priority>
1717
+ <properties>
1718
+ <property name="xpath"><value><![CDATA[
1719
+ //PostfixUnaryExpression
1720
+ [PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'runBlocking']]
1721
+ [PostfixUnarySuffix[1]/CallSuffix]
1722
+ ]]></value></property>
1723
+ </properties>
1724
+ </rule>
1725
+
1726
+ <rule name="NoVarClassParameter" language="kotlin"
1727
+ message="コンストラクタの var は val にし、変更は copy で新しい値を作る"
1728
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1729
+ <description>コンストラクタのプロパティを var で宣言することを禁止する。イベントやコマンドなどの値は不変にする。</description>
1730
+ <priority>3</priority>
1731
+ <properties>
1732
+ <property name="xpath"><value><![CDATA[
1733
+ //ClassParameter[T-VAR]
1734
+ ]]></value></property>
1735
+ </properties>
1736
+ </rule>
1737
+
1738
+ <rule name="NoMutableCollectionFactory" language="kotlin"
1739
+ message="可変コレクションの生成は buildList / buildSet / buildMap の内側に閉じ込める"
1740
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1741
+ <description>buildList / buildSet / buildMap の外で、可変コレクションを生成することを禁止する。</description>
1742
+ <priority>3</priority>
1743
+ <properties>
1744
+ <property name="xpath"><value><![CDATA[
1745
+ //PostfixUnaryExpression
1746
+ [PrimaryExpression/SimpleIdentifier/T-Identifier
1747
+ [@Text = ('mutableListOf', 'mutableSetOf', 'mutableMapOf', 'arrayListOf', 'hashSetOf', 'hashMapOf',
1748
+ 'linkedSetOf', 'linkedMapOf', 'ArrayList', 'HashSet', 'HashMap', 'LinkedHashSet', 'LinkedHashMap')]]
1749
+ [PostfixUnarySuffix/CallSuffix]
1750
+ [not(ancestor::PostfixUnaryExpression
1751
+ [PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = ('buildList', 'buildSet', 'buildMap')]])]
1752
+ ]]></value></property>
1753
+ </properties>
1754
+ </rule>
1755
+
1756
+ <rule name="NoReduce" language="kotlin"
1757
+ message="reduce は空のコレクションで例外を投げる。fold か reduceOrNull を使う"
1758
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1759
+ <description>reduce / reduceRight の呼び出しを禁止する。</description>
1760
+ <priority>3</priority>
1761
+ <properties>
1762
+ <property name="xpath"><value><![CDATA[
1763
+ //PostfixUnaryExpression/PostfixUnarySuffix/NavigationSuffix/SimpleIdentifier/T-Identifier
1764
+ [@Text = ('reduce', 'reduceRight')]
1765
+ ]]></value></property>
1766
+ </properties>
1767
+ </rule>
1768
+
1769
+ <rule name="NoGenericCatch" language="kotlin"
1770
+ message="Exception / Throwable を広く catch せず、Either.catch で値にするか、想定する例外の型だけを catch する"
1771
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1772
+ <description>Exception / Throwable / RuntimeException を catch することを禁止する。</description>
1773
+ <priority>3</priority>
1774
+ <properties>
1775
+ <property name="xpath"><value><![CDATA[
1776
+ //CatchBlock[Type//T-Identifier[@Text = ('Exception', 'Throwable', 'RuntimeException')]]
1777
+ ]]></value></property>
1778
+ </properties>
1779
+ </rule>
1780
+
1781
+ <rule name="NoNestedFlatMap" language="kotlin"
1782
+ message="flatMap の入れ子は、either ブロックと bind()、または sequence ビルダーで平らに書く"
1783
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1784
+ <description>flatMap の中で flatMap を呼ぶことを禁止する。</description>
1785
+ <priority>3</priority>
1786
+ <properties>
1787
+ <property name="xpath"><value><![CDATA[
1788
+ //PostfixUnaryExpression
1789
+ [PostfixUnarySuffix/NavigationSuffix/SimpleIdentifier/T-Identifier[@Text = 'flatMap']]
1790
+ [ancestor::PostfixUnaryExpression
1791
+ [PostfixUnarySuffix/NavigationSuffix/SimpleIdentifier/T-Identifier[@Text = 'flatMap']]]
1792
+ ]]></value></property>
1793
+ </properties>
1794
+ </rule>
1795
+
1796
+ <rule name="NoGlobalScope" language="kotlin"
1797
+ message="GlobalScope を使わず、coroutineScope の中で起動して構造化並行性を保つ"
1798
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1799
+ <description>GlobalScope の使用を禁止する。</description>
1800
+ <priority>2</priority>
1801
+ <properties>
1802
+ <property name="xpath"><value><![CDATA[
1803
+ //PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'GlobalScope']
1804
+ ]]></value></property>
1805
+ </properties>
1806
+ </rule>
1807
+
1808
+ <rule name="NoRawConcurrency" language="kotlin"
1809
+ message="スレッドやロックを直接扱わず、コルーチンと Atomic で共有状態を更新する"
1810
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1811
+ <description>Thread の生成、thread { }、Executors、synchronized、@Synchronized を禁止する。</description>
1812
+ <priority>3</priority>
1813
+ <properties>
1814
+ <property name="xpath"><value><![CDATA[
1815
+ (
1816
+ //PostfixUnaryExpression
1817
+ [PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = ('Thread', 'thread', 'synchronized')]]
1818
+ [PostfixUnarySuffix[1]/CallSuffix]
1819
+ | //PostfixUnaryExpression
1820
+ [PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'Executors']]
1821
+ | //SingleAnnotation/UnescapedAnnotation/UserType/SimpleUserType/SimpleIdentifier/T-Identifier
1822
+ [@Text = 'Synchronized']
1823
+ )
1824
+ ]]></value></property>
1825
+ </properties>
1826
+ </rule>
1827
+
1828
+ <rule name="NoThreadSleep" language="kotlin"
1829
+ message="Thread.sleep はスレッドを塞ぐ。suspend 関数の delay を使い、テストでは runTest の仮想時間を使う"
1830
+ class="net.sourceforge.pmd.lang.rule.xpath.XPathRule">
1831
+ <description>Thread.sleep の呼び出しを禁止する。</description>
1832
+ <priority>3</priority>
1833
+ <properties>
1834
+ <property name="xpath"><value><![CDATA[
1835
+ //PostfixUnaryExpression
1836
+ [PrimaryExpression/SimpleIdentifier/T-Identifier[@Text = 'Thread']]
1837
+ [PostfixUnarySuffix[1]/NavigationSuffix/SimpleIdentifier/T-Identifier[@Text = 'sleep']]
1838
+ ]]></value></property>
1839
+ </properties>
1840
+ </rule>
1841
+ </ruleset>
1842
+ ```
1843
+
1844
+ #### XPath の読み方
1845
+
1846
+ | 式 | 意味 | 根拠 |
1847
+ | --- | --- | --- |
1848
+ | `PropertyDeclaration[@Mutable = true()]` | `var` で宣言されたプロパティ | PMD の Kotlin 対応のページに `@Mutable` と、真偽値は `true()` / `false()` で比べると書かれている |
1849
+ | `not(ancestor::FunctionBody or ...)` | 関数の本体、`init` ブロック、副コンストラクタの中を除く(つまりローカルな `var` を許す) | PMD の `LooseCoupling` が同じ条件でローカルのプロパティを除いている |
1850
+ | `PostfixUnaryOperator[Excl]` | 後置演算子のうち、`!` を子に持つもの(`!!`)。前置の `!b` は `PrefixUnaryOperator` なので対象外 | 文法定義の `postfixUnaryOperator : INCR \| DECR \| EXCL_NO_WS excl` |
1851
+ | `JumpExpression[T-THROW]` | `throw` で始まるジャンプ式 | 文法定義の `jumpExpression : THROW NL* expression`。トークンのノード名は `T-` に字句の名前を付けた形(例: `T-CLASS`、`T-Identifier`) |
1852
+ | `pmd-kotlin:modifiers()` | 修飾子の列。`private` / `protected` / `internal` を持たない関数を「公開」とみなす | PMD の `LooseCoupling` が `pmd-kotlin:modifiers() = 'override'` を使っている |
1853
+ | `SimpleUserType/SimpleIdentifier/T-Identifier[@Text = (...)]` | 型名の識別子が `Mutable*` | 文法定義の `simpleUserType : simpleIdentifier typeArguments?` |
1854
+
1855
+ `Mutable*` の検査は、型名の文字列に対する検査である。次の点に注意する。
1856
+
1857
+ - **型の別名は検出できない。** `typealias Names = MutableList<String>` で包むと見逃す。
1858
+ - **公開かどうかは関数自身の修飾子だけで判断する。** `private` なクラスの中の公開関数も報告する。
1859
+ - **型情報を使っていない。** `auxClasspath` の設定は要らないが、`import ... as` で別名を付けた型は見逃す。
1860
+
1861
+ 副作用の型付けのルール(`SideEffectInNonSuspendFunction`、`NoRunBlocking`)は、呼び出しの式の形を読む。
1862
+
1863
+ | 式 | 意味 | 根拠 |
1864
+ | --- | --- | --- |
1865
+ | `PostfixUnaryExpression` | `a.b(...)` や `f(...)` のような、先頭の要素と後ろに続く接尾辞の並び | `System.currentTimeMillis()` の構文木は、`PrimaryExpression`(`System`)、`NavigationSuffix`(`.currentTimeMillis`)、`CallSuffix`(`()`)の順に並ぶ。PMD の `ast-dump` で確認した |
1866
+ | `PostfixUnarySuffix[1]/CallSuffix` | 先頭の要素の直後が呼び出し。`println(...)` や `runBlocking { }` のような修飾なしの呼び出し | 同上 |
1867
+ | `PostfixUnarySuffix[1]/NavigationSuffix/...` と `PostfixUnarySuffix[2]/CallSuffix` | `名前.メンバ(...)` の形の呼び出し | 同上 |
1868
+ | `ancestor::FunctionDeclaration[1]` | その呼び出しを直接囲む関数(一番内側) | XPath の標準の軸 |
1869
+ | `Modifiers//T-SUSPEND` | その関数が `suspend` で修飾されている | `suspend` 関数の構文木に `T-SUSPEND` が現れる(`ast-dump` で確認) |
1870
+
1871
+ このルールが検出する呼び出しは、次のとおりである。型名と関数名の文字列で判断するので、一覧にないものは検出しない。
1872
+
1873
+ | 種類 | 検出する呼び出し |
1874
+ | --- | --- |
1875
+ | 標準出力 | `println(...)`、`print(...)` |
1876
+ | 時計 | `System.currentTimeMillis()`、`System.nanoTime()`、`Instant.now()`、`LocalDate.now()`、`LocalDateTime.now()`、`LocalTime.now()`、`ZonedDateTime.now()`、`OffsetDateTime.now()` |
1877
+ | 乱数 | `Random.next...(...)`(`Random.nextInt(1, 7)` など) |
1878
+ | 識別子の生成 | `UUID.randomUUID()` |
1879
+
1880
+ 次の点に注意する。
1881
+
1882
+ - **関数の外は対象外。** トップレベルのプロパティの初期化(`val top = System.currentTimeMillis()`)のように、囲む関数がない場所は見逃す。
1883
+ - **`Random.Default.nextInt()` のように、途中に要素が挟まる形は見逃す。**
1884
+ - **`suspend` の関数の中のラムダは許す。** 一番内側の関数が `suspend` であれば報告しない。
1885
+ - **`suspend` にしても、純粋になるわけではない。** このルールが求めるのは、副作用を持つ処理を `suspend` 関数の中、または引数として外から受け取る関数(`clock: () -> Long` のような形)に置くことで、呼び出し位置を型で限定することである。
1886
+ - **`fun main()` や HTTP の入口など、境界のコードは報告される。** 除外パターンで外すか、ルールセットを分ける。
1887
+
1888
+ #### 標準ルールと合わせて有効にする
1889
+
1890
+ Kotlin 用の標準ルールと、上のカスタムルールセットを並べて参照する。
1891
+
1892
+ ```xml
1893
+ <?xml version="1.0"?>
1894
+ <ruleset name="Project Kotlin Rules"
1895
+ xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
1896
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1897
+ xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
1898
+ <description>プロジェクト用の Kotlin ルールセット</description>
1899
+
1900
+ <rule ref="category/kotlin/bestpractices.xml"/>
1901
+ <rule ref="category/kotlin/errorprone.xml"/>
1902
+ <rule ref="config/pmd/fp-kotlin-ruleset.xml"/>
1903
+ </ruleset>
1904
+ ```
1905
+
1906
+ 境界のコード(HTTP の入口、第三者のライブラリとの接続)やテストでは、例外を投げる必要がある。その層だけを外すには、ルールセットのファイルを分けるか、PMD の除外パターンを使う。`@Suppress` を教材や記事のコードに写り込ませないためにも、層ごとに外すほうがよい。
1907
+
1908
+ #### ルールの確認
1909
+
1910
+ ルールは、使い始める前にテストファーストで確かめる。違反するコードと、しないコードを 1 つのファイルに並べ、期待どおりの行だけが報告されることを見る。
1911
+
1912
+ ```kotlin
1913
+ package sample
1914
+
1915
+ class Counter {
1916
+ var count = 0 // NoVarProperty
1917
+ val ok = 1
1918
+ }
1919
+
1920
+ fun sum(numbers: List<Int>): Int {
1921
+ var total = 0 // ローカルな var は許す
1922
+ for (n in numbers) total += n
1923
+ return total
1924
+ }
1925
+
1926
+ fun first(list: List<String>?): String = list!![0] // NoNotNullAssertion
1927
+
1928
+ fun notNull(b: Boolean): Boolean = !b // 前置の ! は対象外
1929
+
1930
+ fun fail(): Nothing = throw IllegalStateException("boom") // NoThrow
1931
+
1932
+ fun add(items: MutableList<String>): MutableList<String> = items // NoMutableCollectionInSignature(引数と戻り値で 2 件)
1933
+
1934
+ private fun addPrivate(items: MutableList<String>): Int = items.size // private は対象外
1935
+
1936
+ fun readOnly(items: List<String>): List<String> = items + "x" // 読み取り専用は対象外
1937
+
1938
+ fun now(): Long = System.currentTimeMillis() // SideEffectInNonSuspendFunction
1939
+ fun log(message: String) { println(message) } // SideEffectInNonSuspendFunction
1940
+ suspend fun castTheDie(): Int = Random.nextInt(1, 7) // suspend 関数の中は対象外
1941
+ fun pureScore(word: String): Int = word.length // 純粋関数は対象外
1942
+ fun blocking(): Int = runBlocking { 1 } // NoRunBlocking
1943
+ data class Counter2(var x: Int) // NoVarClassParameter
1944
+ fun words(): List<String> { val l = mutableListOf("a"); return l } // NoMutableCollectionFactory
1945
+ fun squares(n: Int): List<Int> = buildList { val tmp = mutableListOf(n); addAll(tmp) } // buildList の中は対象外
1946
+ fun biggest(n: List<Int>): Int = n.reduce { a, b -> if (a > b) a else b } // NoReduce
1947
+ fun safe(): Int = try { 1 } catch (e: Exception) { 0 } // NoGenericCatch
1948
+ fun nested(a: List<Int>): List<Int> = a.flatMap { x -> listOf(x).flatMap { y -> listOf(y) } } // NoNestedFlatMap
1949
+ fun launchAll() { GlobalScope.launch { } } // NoGlobalScope
1950
+ fun spawn() { thread { } } // NoRawConcurrency
1951
+ @Synchronized fun sync() {} // NoRawConcurrency
1952
+ fun nap() { Thread.sleep(10) } // NoThreadSleep
1953
+ ```
1954
+
1955
+ ```bash
1956
+ pmd check -d src -R fp-kotlin-ruleset.xml -f text --no-cache
1957
+ ```
1958
+
1959
+ 期待する結果は、`NoVarProperty` が 1 件(4 行目)、`NoNotNullAssertion` が 1 件(14 行目)、`NoThrow` が 1 件(18 行目)、`NoMutableCollectionInSignature` が 2 件(20 行目、引数と戻り値)、`SideEffectInNonSuspendFunction` が 2 件(26・27 行目)、`NoRunBlocking` が 1 件(30 行目)、`NoVarClassParameter` が 1 件(31 行目)、`NoMutableCollectionFactory` が 1 件(32 行目)、`NoReduce` が 1 件(34 行目)、`NoGenericCatch` が 1 件(35 行目)、`NoNestedFlatMap` が 1 件(36 行目)、`NoGlobalScope` が 1 件(37 行目)、`NoRawConcurrency` が 2 件(38・39 行目)、`NoThreadSleep` が 1 件(40 行目)である。PMD 7.29.0 で実行したところ、この 17 件だけが報告された。`suspend` 関数の中の `Random.nextInt`、`buildList` の内側の `mutableListOf`(33 行目)、純粋関数は報告されない。
1960
+
1961
+ ルールの `message` に `{` と `}` を書いてはいけない。PMD が `MessageFormat` として解釈し、`can't parse argument number` の内部エラーで処理が失敗する。コード片を示したいときは、`description` に書くか、波括弧を使わない言い方にする(`either ブロック`、`sequence ビルダー`)。件数が合わないときは、PMD の `designer`(構文木を見るツール)で、実際のノード名を確かめて XPath を直す。
1962
+
1963
+ 実行時に `No auxClasspath configured for Kotlin; type resolution will not work.` という警告が出る。これらのルールは型情報を使わない構文の検査なので、無視してよい。
1964
+
1965
+ 追加で、次の境界のケースも確かめた。
1966
+
1967
+ | コード | 結果 |
1968
+ | --- | --- |
1969
+ | トップレベルの `var`、`object` 内の `var` | 報告される |
1970
+ | `init` ブロック内の `var`、関数内の `var` | 報告されない |
1971
+ | クラスの公開メソッドの `MutableList` 引数 | 報告される |
1972
+ | `private` 関数の `MutableMap` 引数 | 報告されない |
1973
+ | `typealias N = MutableList<String>` を引数の型に使う | 報告されない(見逃す。前述の制限) |
1974
+ | 前置の `!true` | 報告されない |
1975
+ | `i!!.plus(1)` | 報告される |
1976
+ | 本体にブロックを持つローカル関数の `MutableSet` 引数 | 報告されない |
1977
+ | 非 `suspend` のメソッドの `Instant.now()`、`System.nanoTime()`、`UUID.randomUUID()`、`print("a")` | 報告される |
1978
+ | `suspend` 関数の中の `println`、ラムダ内の `Random.nextInt()` | 報告されない |
1979
+ | 時計を `clock: () -> Long` で受け取り `clock()` を呼ぶ関数 | 報告されない |
1980
+ | `Random.Default.nextInt()`、トップレベルプロパティの `System.currentTimeMillis()` | 報告されない(見逃す) |
1981
+
1982
+ なお、式本体のローカル関数(`fun h() { fun inner(a: MutableSet<Int>) = 1 }`)を含むファイルでは、PMD 7.29.0 が内部エラー(`Parameter count mismatch in !debug only!`)で処理に失敗した。これらのルールの問題ではなく、PMD 側の挙動である。同じ形のコードがあるときは、PMD の更新を試す。
1983
+
1984
+ #### detekt で実装する場合
1985
+
1986
+ 同じ規約を detekt のカスタムルールとして実装することもできる。detekt のルールは Kotlin 製のプラグイン(`Rule` を継承したクラスと `RuleSetProvider`、`detektPlugins` での登録)になるため、Gradle のサブプロジェクトとテストが要る。XPath で足りる規約は PMD、型情報や複雑な判断が要る規約は detekt、と使い分ける。
1987
+
1988
+ #### 導入の進め方
1989
+
1990
+ 1. まず違反の件数を数える(9.5 節の「実際に起きている問題を数える」)。
1991
+ 2. 件数が多いルールは、新しいコードだけに適用する。
1992
+ 3. ルールを増やすのは、同じ指摘をレビューで繰り返したときに限る。
1993
+
1994
+ ---
1995
+
1996
+ ## 10. 判断の指針
1997
+
1998
+ ### 10.1 自前の抽象か、ライブラリか
1999
+
2000
+ 出典の 2 つの記事は、同じ関数型の考え方を異なる手段で実現している。
2001
+
2002
+ | 観点 | 自前で組み立てる | Arrow と coroutines に委ねる |
2003
+ | --- | --- | --- |
2004
+ | 副作用 | 小さな `IO<A>` を実装する | `suspend` 関数 |
2005
+ | エラー | `Either` を自前で定義する | Arrow の `Either`、`Raise`、`either { }` |
2006
+ | 並行 | スレッドとロックを直接扱う | 構造化並行性、`parMap`、`raceN` |
2007
+ | 利点 | 仕組みが見えて理解が深まる | 実務で使える完成度、コードが短い |
2008
+ | 向く場面 | 学習・仕組みの理解 | 実際のアプリケーション |
2009
+
2010
+ 学習では自前の `IO` が仕組みを理解する助けになる。実務では Kotlin 標準の `suspend` と Arrow を使う。
2011
+
2012
+ ### 10.2 落とし穴
2013
+
2014
+ | 落とし穴 | 対処 |
2015
+ | --- | --- |
2016
+ | `List` を不変だと思い込む | 読み取り専用ビューにすぎないと知り、コピーするか可変参照を共有しない |
2017
+ | `val` を不変の保証だと思い込む | `val` は再代入を防ぐだけ。参照先の型で不変性を確かめる |
2018
+ | 式本体で書いたから純粋だと思う | 乱数、時計、I/O は隠れた入力。外から引数で渡す |
2019
+ | `reduce` を空のリストに使う | 空でも使えるなら `fold`、結果がないなら `reduceOrNull` |
2020
+ | `flatMap` を連鎖させてネストが深くなる | `either { }` と `bind()`、`sequence { }` で平らに書く |
2021
+ | 検証で最初のエラーだけ返す | 独立した検証は `zipOrAccumulate` で蓄積する |
2022
+ | `Flow` を一度だけ実行されると思う | コールドなので `collect` のたびに最初から実行される |
2023
+ | `async` を `coroutineScope` の外で起動する | 構造化並行性の中で起動し、失敗とキャンセルを伝播させる |
2024
+ | `Int` の合計がオーバーフローする | 合計は `Long` で行い、プロパティベーステストで境界を探る |
2025
+
2026
+ ### 10.3 導入の順序
2027
+
2028
+ 一度にすべてを取り入れる必要はない。効果が大きく、リスクの小さい順に進める。
2029
+
2030
+ 1. `val`、式本体関数、`data class` と `copy`、読み取り専用コレクションを使う。
2031
+ 2. 副作用のある処理と純粋な計算を、別の関数に分ける。
2032
+ 3. 例外の代わりに nullable と `Either` を返す。
2033
+ 4. 副作用を境界に寄せ、`suspend` 関数や関数の引数として外から渡す。
2034
+ 5. 検証や並行処理に Arrow の道具を使う。
2035
+ 6. 状態の更新をイベントの列にする設計は、変更の履歴が価値を持つ領域に限って検討する。
2036
+
2037
+ ---
2038
+
2039
+ ## 11. 参考資料
2040
+
2041
+ - Grokking Functional Programming Kotlin 版: 純粋関数から並行処理、テスト戦略までを章立てで学ぶ記事(出典)
2042
+ - Zettai Kotlin FP 版(From Objects to Functions 演習): ToDo アプリを題材に、HTTP、ドメインモデリング、イベントソーシング、関数型エラーハンドリングまでを実装する連載(出典)
2043
+ - 静的解析の節の出典: Zettai 連載の ADR-004(静的解析を `check` に組み込まない決定)と、Java 版サンプル(getting-started-tdd)の PMD `ruleset.xml`
2044
+ - 上記 2 つのシリーズへのリンクは [記事](../article/index.md) にある。
2045
+ - [Arrow](https://arrow-kt.io/): Kotlin 向けの関数型プログラミングライブラリ
2046
+ - [kotlinx.coroutines](https://kotlinlang.org/docs/coroutines-overview.html): Kotlin のコルーチン
2047
+ - [detekt](https://detekt.dev/)、[ktlint](https://pinterest.github.io/ktlint/)、[PMD](https://pmd.github.io/): 静的解析ツール
2048
+ - 関連ガイド: [テスト戦略ガイド](./テスト戦略ガイド.md)、[コーディングとテストガイド](./コーディングとテストガイド.md)、[ドメインモデル設計ガイド](./ドメインモデル設計ガイド.md)