@k2works/claude-code-booster 6.6.1 → 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,2117 @@
1
+ ---
2
+ type: Reference
3
+ title: "関数型プログラミングガイド(Rust 編)"
4
+ description: "Rust の標準ライブラリ、tokio、futures で関数型プログラミングを実践するためのガイド。純粋関数、所有権と不変性、Option と Result によるエラーの型表現、async による副作用管理、並行処理、イベントソーシングによるアーキテクチャ設計、clippy によるルール設定までを一本の流れで整理する。"
5
+ tags: [reference, rust, functional-programming]
6
+ status: draft
7
+ generated: { by: claude-sonnet-5-5, at: 2026-11-13T00:00:00Z }
8
+ ---
9
+
10
+ # 関数型プログラミングガイド(Rust 編)
11
+
12
+ ## このガイドについて
13
+
14
+ 関数型プログラミング(FP)は、状態の書き換えではなく値の変換でプログラムを組み立てる考え方である。変更を楽に安全にできるソフトウェアを作るための技術的な手段のひとつであり、副作用を境界に押し出し、型で失敗を表し、小さな関数を合成する。
15
+
16
+ 本ガイドは、Rust でこの考え方を実践するための要点を整理する。構成は [関数型プログラミングガイド(Kotlin 編)](./関数型プログラミングガイド_Kotlin編.md) に合わせている。次の 2 つの学習記事を元にしている。
17
+
18
+ | 出典 | 内容 | 本ガイドでの扱い |
19
+ | --- | --- | --- |
20
+ | Grokking Functional Programming Rust 版(全 12 章) | 純粋関数、イミュータブルなデータ、Option/Result、async、ストリーム、並行処理、実践的なアプリケーション | 第 1〜5 章の骨格 |
21
+ | Zettai Rust 版(全 13 章、From Objects to Functions 演習) | ToDo アプリを題材にした HTTP、ドメインモデリング、イベントソーシング、関数型エラーハンドリング、アプリカティブ、関数型 JSON | 第 6〜8 章の骨格 |
22
+
23
+ - **対象**: 他の言語で書いた経験があり、所有権のある言語で関数型の設計をしたい開発者。所有権・借用・ライフタイムの基礎は知っているものとする。
24
+ - **使うもの**: 第 1〜3 章は Rust の標準ライブラリのみ。第 4〜5 章は [tokio](https://tokio.rs/) と [futures](https://docs.rs/futures/) を使う。Zettai 版の本体は同期のコードで、依存は `tiny_http`、`postgres`、`tera` だけである。
25
+ - **コード例**: 出典の記事から転記している。本文に載せたコードは、このガイドのために書き下ろしたものではない。ただし第 9 章の clippy の設定と確認用コードだけは例外で、このガイドのために書き、clippy 0.1.91 で実行して確認した。詳細な解説と演習問題は出典の記事を参照する。
26
+
27
+ ### Kotlin 編との違い
28
+
29
+ Rust は、Kotlin 編で自前に作ったり Arrow に頼ったりした多くのものを、言語やコンパイラが与える。代わりに所有権・借用・ライフタイムという制約が入る。
30
+
31
+ | | Kotlin | Rust |
32
+ | --- | --- | --- |
33
+ | 不変性 | `val` は再代入を防ぐだけ。`List` は読み取り専用のビュー | 束縛はデフォルトで不変。変更は `mut` で明示する |
34
+ | 失敗の型 | nullable、Arrow の `Either` と `Raise` | `Option`、`Result`、`?` が言語にある |
35
+ | 代数的データ型 | `sealed interface` | `enum` |
36
+ | 副作用の型付け | `suspend` | `async fn`(本体は `.await` まで実行されない)。ただし Zettai 版は同期のコードを選んだ |
37
+ | 並行処理の安全 | 構造化並行性 | 所有権、`Send` と `Sync`(データ競合がコンパイルで止まる) |
38
+ | テストの土台 | Kotest など | `#[test]` が言語にある |
39
+ | 静的解析 | detekt、ktlint、PMD | rustfmt、clippy(コンパイラの lint を含む) |
40
+
41
+ ## 全体像
42
+
43
+ ```mermaid
44
+ flowchart LR
45
+ A[1. 基礎<br>純粋関数] --> B[2. 関数型スタイル<br>所有権と不変データ<br>高階関数]
46
+ B --> C[3. 型で表すエラー<br>Option Result enum]
47
+ C --> D[4. 副作用の管理<br>async Stream]
48
+ D --> E[5. 並行処理<br>Arc Mutex チャネル]
49
+ C --> F[6. 代数構造の実用<br>モノイド ファンクタ モナド<br>アプリカティブ]
50
+ F --> G[7. アーキテクチャ<br>イベントソーシング CQRS]
51
+ D --> G
52
+ E --> G
53
+ G --> H[8. テスト戦略<br>スタブ プロパティテスト 法則]
54
+ H --> I[9. 静的解析<br>rustfmt clippy]
55
+ ```
56
+
57
+ | 章 | 主題 | 一言で言うと |
58
+ | --- | --- | --- |
59
+ | 1 | 基礎 | 同じ入力には同じ出力。外に触れない |
60
+ | 2 | 関数型スタイル | 変更せず、新しい値を作る。関数を値として渡す |
61
+ | 3 | 型で表すエラー | 失敗を panic ではなく戻り値の型で表す |
62
+ | 4 | 副作用の管理 | 副作用を「実行しない記述」として値にする |
63
+ | 5 | 並行処理 | 並行処理も所有権と型で安全にする |
64
+ | 6 | 代数構造の実用 | 合成のパターンに名前を付け、判断の手順で使い分ける |
65
+ | 7 | アーキテクチャ | 状態を事実(イベント)の列として扱う |
66
+ | 8 | テスト戦略 | 純粋関数の性質と、代数構造の法則をテストで言い切る |
67
+ | 9 | 静的解析とルール設定 | 関数型の規約を clippy で機械に守らせる |
68
+
69
+ ---
70
+
71
+ ## 1. 基礎: 純粋関数
72
+
73
+ ### 1.1 命令型と関数型
74
+
75
+ 命令型は「どうやるか」の手順を書き、関数型は「何であるか」の定義を書く。
76
+
77
+ ```rust
78
+ // 命令型スタイル: ステップバイステップで計算
79
+ fn calculate_score_imperative(word: &str) -> usize {
80
+ let mut score = 0;
81
+ for _ in word.chars() {
82
+ score += 1;
83
+ }
84
+ score
85
+ }
86
+ ```
87
+
88
+ ```rust
89
+ // 関数型スタイル: 宣言的に記述
90
+ fn word_score(word: &str) -> usize {
91
+ word.len()
92
+ }
93
+ ```
94
+
95
+ Rust は、束縛がデフォルトでイミュータブルである。変更が必要な場所は `mut` と書いて明示する。
96
+
97
+ ```rust
98
+ let x = 5; // イミュータブル
99
+ // x = 6; // コンパイルエラー!
100
+
101
+ let mut y = 5; // ミュータブル(明示的に指定)
102
+ y = 6; // OK
103
+ ```
104
+
105
+ Kotlin の `val` は再代入を防ぐだけで、参照先の不変性までは保証しない。Rust は、不変な束縛を通して中身を変更することもできない。内部可変性を持つ型(`Mutex` や `RefCell` など)を除けば、変更を許す場所は `mut` が付いた場所だけである。
106
+
107
+ ### 1.2 純粋関数
108
+
109
+ 純粋関数は次の 2 つを満たす。
110
+
111
+ 1. 同じ入力には常に同じ出力を返す。
112
+ 2. 副作用がない(外部状態を変更しない)。
113
+
114
+ ```rust
115
+ /// 純粋関数: 同じ入力に対して常に同じ出力
116
+ fn add(a: i32, b: i32) -> i32 {
117
+ a + b
118
+ }
119
+
120
+ /// 純粋関数: 文字 'a' を除外したワードスコア
121
+ fn word_score(word: &str) -> usize {
122
+ word.chars().filter(|&c| c != 'a').count()
123
+ }
124
+
125
+ // テスト
126
+ assert_eq!(word_score("Scala"), 3); // "Scl" → 3文字
127
+ assert_eq!(word_score("function"), 8); // 'a' なし → 8文字
128
+ assert_eq!(word_score(""), 0); // 空文字 → 0文字
129
+ ```
130
+
131
+ システム時計は隠れた入力であり、引数が同じでも結果が変わる。
132
+
133
+ ```rust
134
+ // 不純な関数 - 呼び出すたびに異なる値を返す
135
+ fn current_timestamp() -> u64 {
136
+ use std::time::{SystemTime, UNIX_EPOCH};
137
+ SystemTime::now()
138
+ .duration_since(UNIX_EPOCH)
139
+ .unwrap()
140
+ .as_secs()
141
+ }
142
+ ```
143
+
144
+ ### 1.3 所有権と純粋性
145
+
146
+ Rust では、関数のシグネチャから、引数をどう扱うかが分かる。`&str` や `&[i32]` で借りる関数は、元のデータを変更できない。新しい値は戻り値として返す。
147
+
148
+ ```rust
149
+ /// 借用を使った純粋関数(元のデータを変更しない)
150
+ fn count_vowels(s: &str) -> usize {
151
+ s.chars()
152
+ .filter(|c| matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u'))
153
+ .count()
154
+ }
155
+
156
+ /// Vec を変換する純粋関数(新しい Vec を返す)
157
+ fn double_all(numbers: &[i32]) -> Vec<i32> {
158
+ numbers.iter().map(|x| x * 2).collect()
159
+ }
160
+
161
+ // 元のデータは変更されない
162
+ let numbers = vec![1, 2, 3];
163
+ let doubled = double_all(&numbers);
164
+ assert_eq!(doubled, vec![2, 4, 6]);
165
+ assert_eq!(numbers, vec![1, 2, 3]); // 元のデータはそのまま
166
+ ```
167
+
168
+ ショッピングカートの例では、状態を持つクラスの代わりに、カートの中身を引数で受け取る純粋関数を合成する。
169
+
170
+ ```rust
171
+ /// ショッピングカートの割引率を計算(純粋関数)
172
+ fn get_discount_percentage(items: &[&str]) -> i32 {
173
+ if items.contains(&"Book") {
174
+ 5
175
+ } else {
176
+ 0
177
+ }
178
+ }
179
+
180
+ /// 割引額を計算(純粋関数)
181
+ fn calculate_discount(total: i32, percentage: i32) -> i32 {
182
+ total * percentage / 100
183
+ }
184
+
185
+ /// 最終価格を計算(純粋関数の合成)
186
+ fn calculate_final_price(total: i32, items: &[&str]) -> i32 {
187
+ let discount_percentage = get_discount_percentage(items);
188
+ let discount = calculate_discount(total, discount_percentage);
189
+ total - discount
190
+ }
191
+
192
+ // テスト
193
+ assert_eq!(calculate_final_price(100, &["Apple", "Book"]), 95);
194
+ assert_eq!(calculate_final_price(100, &["Apple"]), 100);
195
+ ```
196
+
197
+ ### 1.4 参照透過性
198
+
199
+ 純粋関数の呼び出しは、その結果の値に置き換えても、プログラムの意味が変わらない。これを参照透過性と呼ぶ。
200
+
201
+ ```rust
202
+ // word_score("Scala") は常に 3 を返す
203
+ let score1 = word_score("Scala");
204
+ let score2 = word_score("Scala");
205
+ // score1 と score2 は常に同じ値(3)
206
+
207
+ // 以下の2つは同等
208
+ let total1 = word_score("Scala") + word_score("Java");
209
+ let total2 = 3 + 2; // word_score の結果で置き換え可能
210
+ assert_eq!(total1, total2);
211
+ ```
212
+
213
+ **純粋関数の利点**: テストが入力と出力の確認だけで済む。並行に呼んでも安全である。呼び出しを値に置き換えても意味が変わらないので、読んで理解しやすい。
214
+
215
+ ---
216
+
217
+ ## 2. 関数型スタイル: 所有権と不変データ、高階関数
218
+
219
+ ### 2.1 新しい値を作る
220
+
221
+ イミュータブルなデータを「変更する」とは、変更を加えた新しい値を作ることである。元の値はそのまま残る。
222
+
223
+ ```rust
224
+ let list = vec!["Apple", "Book"];
225
+ // list.push("Mango"); // コンパイルエラー!不変だから
226
+
227
+ // 新しい Vec を作成する
228
+ let mut new_list = list.clone();
229
+ new_list.push("Mango");
230
+
231
+ assert_eq!(list.len(), 2); // 元のリストは変わらない
232
+ assert_eq!(new_list.len(), 3); // 新しいリストに追加された
233
+ ```
234
+
235
+ この変更を関数にして、局所的な可変操作を関数の内側に閉じ込める。呼び出し側からは、新しい値が返るだけに見える。
236
+
237
+ ```rust
238
+ pub fn appended<T: Clone>(list: &[T], element: T) -> Vec<T> {
239
+ let mut result = list.to_vec();
240
+ result.push(element);
241
+ result
242
+ }
243
+
244
+ let apple_book = vec!["Apple", "Book"];
245
+ let apple_book_mango = appended(&apple_book, "Mango");
246
+
247
+ assert_eq!(apple_book.len(), 2); // 元のリストは変わらない
248
+ assert_eq!(apple_book_mango.len(), 3);
249
+ assert_eq!(apple_book_mango, vec!["Apple", "Book", "Mango"]);
250
+ ```
251
+
252
+ ```rust
253
+ pub fn first_two<T: Clone>(list: &[T]) -> Vec<T> {
254
+ list.iter().take(2).cloned().collect()
255
+ }
256
+
257
+ pub fn last_two<T: Clone>(list: &[T]) -> Vec<T> {
258
+ let len = list.len();
259
+ if len <= 2 {
260
+ list.to_vec()
261
+ } else {
262
+ list[len - 2..].to_vec()
263
+ }
264
+ }
265
+
266
+ assert_eq!(first_two(&["a", "b", "c"]), vec!["a", "b"]);
267
+ assert_eq!(last_two(&["a", "b", "c"]), vec!["b", "c"]);
268
+ ```
269
+
270
+ Kotlin では `List` が読み取り専用のビューにすぎず、元の `MutableList` の変更が見えてしまう。Rust では、借用している間は元の値を変更できない(借用規則)ので、このすり替わりが起きない。借用を手放さず `&mut` で変更しようとすると、コンパイルが止まる。
271
+
272
+ ### 2.2 関数を値として渡す
273
+
274
+ 関数は引数にも戻り値にもなる。変わる部分を関数として外から渡すと、同じ枠組みで振る舞いを差し替えられる。Rust では、関数を受け取る型を `Fn` トレイトで表す。
275
+
276
+ ```rust
277
+ pub fn score(word: &str) -> usize {
278
+ word.chars().filter(|&c| c != 'a').count()
279
+ }
280
+
281
+ pub fn sort_by_score<T, F, K>(list: &[T], key_fn: F) -> Vec<T>
282
+ where
283
+ T: Clone,
284
+ F: Fn(&T) -> K,
285
+ K: Ord,
286
+ {
287
+ let mut result = list.to_vec();
288
+ result.sort_by_key(|item| key_fn(item));
289
+ result
290
+ }
291
+
292
+ let words = vec!["rust", "java"];
293
+ let sorted = sort_by_score(&words, |w| score(w));
294
+ assert_eq!(sorted, vec!["java", "rust"]);
295
+ // java: 2文字 (j, v), rust: 4文字 (r, u, s, t)
296
+ ```
297
+
298
+ | トレイト | 意味 |
299
+ | --- | --- |
300
+ | `Fn` | 何度でも呼べる。キャプチャした値を読むだけ |
301
+ | `FnMut` | 何度でも呼べるが、キャプチャした値を変更する |
302
+ | `FnOnce` | 1 回だけ呼べる。キャプチャした値を消費する |
303
+
304
+ 純粋な関数を受け取るなら、できるだけ `Fn` で受け取る。
305
+
306
+ ### 2.3 map / filter / fold
307
+
308
+ | 操作 | 役割 |
309
+ | --- | --- |
310
+ | `map` | 各要素を変換する(要素数は同じ) |
311
+ | `filter` | 条件に合う要素だけ残す |
312
+ | `fold` | 初期値から畳み込んで 1 つの値にする |
313
+
314
+ ```rust
315
+ let lengths: Vec<usize> = vec!["scala", "rust", "ada"]
316
+ .iter()
317
+ .map(|s| s.len())
318
+ .collect();
319
+ assert_eq!(lengths, vec![5, 4, 3]);
320
+
321
+ let doubles: Vec<i32> = vec![5, 1, 2, 4, 0]
322
+ .iter()
323
+ .map(|&i| i * 2)
324
+ .collect();
325
+ assert_eq!(doubles, vec![10, 2, 4, 8, 0]);
326
+ ```
327
+
328
+ ```rust
329
+ let odds: Vec<i32> = vec![5, 1, 2, 4, 0]
330
+ .iter()
331
+ .filter(|&&i| i % 2 == 1)
332
+ .cloned()
333
+ .collect();
334
+ assert_eq!(odds, vec![5, 1]);
335
+ ```
336
+
337
+ ```rust
338
+ let sum = vec![5, 1, 2, 4, 100]
339
+ .iter()
340
+ .fold(0, |acc, &i| acc + i);
341
+ assert_eq!(sum, 112);
342
+
343
+ let max = vec![5, 1, 2, 4, 15]
344
+ .iter()
345
+ .fold(i32::MIN, |max, &i| if i > max { i } else { max });
346
+ assert_eq!(max, 15);
347
+ ```
348
+
349
+ Rust のイテレータは遅延評価で、`collect` や `fold` などで消費するまで何も実行されない。
350
+
351
+ ### 2.4 関数を返す関数とカリー化
352
+
353
+ 条件を関数として組み立てて返すと、同じ `filter` に別の条件を渡せる。戻り値の型には `impl Fn` を使う。
354
+
355
+ ```rust
356
+ pub fn larger_than(n: i32) -> impl Fn(i32) -> bool {
357
+ move |i| i > n
358
+ }
359
+
360
+ let larger_than_4 = larger_than(4);
361
+ assert!(larger_than_4(5));
362
+ assert!(!larger_than_4(4));
363
+
364
+ // filter で使う場合
365
+ let large: Vec<i32> = vec![5, 1, 2, 4, 0]
366
+ .into_iter()
367
+ .filter(|&x| larger_than(4)(x))
368
+ .collect();
369
+ assert_eq!(large, vec![5]);
370
+ ```
371
+
372
+ 引数を 1 つずつ受け取る形(カリー化)にすると、先頭の引数だけ与えた部分適用で、新しい関数を作れる。
373
+
374
+ ```rust
375
+ // カリー化された関数
376
+ pub fn curried_add(a: i32) -> impl Fn(i32) -> i32 {
377
+ move |b| a + b
378
+ }
379
+
380
+ let add_10 = curried_add(10);
381
+ assert_eq!(add_10(5), 15);
382
+ assert_eq!(add_10(20), 30);
383
+ ```
384
+
385
+ `move` は、クロージャが外の値を借りずに所有することを指定する。関数から返すクロージャには、所有して持ち出すために `move` が要る。
386
+
387
+ ### 2.5 flat_map
388
+
389
+ `map` で「コレクションを返す関数」を適用すると結果がネストする。`flat_map` は適用と平坦化を一度に行う。
390
+
391
+ ```rust
392
+ #[derive(Debug, Clone, PartialEq)]
393
+ pub struct Book {
394
+ pub title: String,
395
+ pub authors: Vec<String>,
396
+ }
397
+
398
+ let books = vec![
399
+ Book::new("FP in Scala", vec!["Chiusano", "Bjarnason"]),
400
+ Book::new("The Hobbit", vec!["Tolkien"]),
401
+ ];
402
+
403
+ // map だけだとネストする
404
+ let author_lists: Vec<Vec<String>> = books.iter().map(|b| b.authors.clone()).collect();
405
+ // [["Chiusano", "Bjarnason"], ["Tolkien"]]
406
+
407
+ // flatten で平坦化
408
+ let authors: Vec<String> = author_lists.into_iter().flatten().collect();
409
+ // ["Chiusano", "Bjarnason", "Tolkien"]
410
+ ```
411
+
412
+ ```rust
413
+ // flat_map で同じことを1行で
414
+ let authors: Vec<String> = books
415
+ .iter()
416
+ .flat_map(|book| book.authors.clone())
417
+ .collect();
418
+ assert_eq!(authors, vec!["Chiusano", "Bjarnason", "Tolkien"]);
419
+ ```
420
+
421
+ `flat_map` を入れ子にすると、クロージャが外の値を借りるため、`move` と `clone` が増えて読みにくくなる。
422
+
423
+ ```rust
424
+ // ネストした flat_map
425
+ let recommendations: Vec<String> = books
426
+ .iter()
427
+ .flat_map(|book| {
428
+ let book_title = book.title.clone();
429
+ book.authors.iter().flat_map(move |author| {
430
+ let author_name = author.clone();
431
+ let book_title_inner = book_title.clone();
432
+ book_adaptations(&author_name).into_iter().map(move |movie| {
433
+ format!(
434
+ "You may like {}, because you liked {}'s {}",
435
+ movie.title, author_name, book_title_inner
436
+ )
437
+ })
438
+ })
439
+ })
440
+ .collect();
441
+ ```
442
+
443
+ Rust には Scala の for 内包表記に当たる構文がない。2 つの組み合わせ程度なら、`flat_map` と `map` のイテレータチェーンで足りる。
444
+
445
+ ```rust
446
+ // 全組み合わせを生成
447
+ pub fn all_combinations(xs: &[i32], ys: &[i32]) -> Vec<i32> {
448
+ xs.iter()
449
+ .flat_map(|&x| ys.iter().map(move |&y| x + y))
450
+ .collect()
451
+ }
452
+
453
+ let result = all_combinations(&[1, 2], &[10, 20]);
454
+ assert_eq!(result, vec![11, 21, 12, 22]);
455
+ ```
456
+
457
+ ---
458
+
459
+ ## 3. 型で表すエラー
460
+
461
+ panic は関数のシグネチャに現れず、呼び出し側が備えられない。失敗の可能性を戻り値の型に出すと、扱いをコンパイラが強制する。
462
+
463
+ ### 3.1 Option
464
+
465
+ 値がないことだけを表すなら、標準の `Option<T>` を使う。次の実装は、前提が崩れると `panic!` する。
466
+
467
+ ```rust
468
+ // panic! が発生する可能性あり
469
+ pub fn parse_show_unsafe(raw_show: &str) -> (&str, i32, i32) {
470
+ let bracket_open = raw_show.find('(').unwrap(); // パニックの可能性
471
+ let bracket_close = raw_show.find(')').unwrap(); // パニックの可能性
472
+ let dash = raw_show.find('-').unwrap(); // パニックの可能性
473
+
474
+ let name = raw_show[..bracket_open].trim();
475
+ let year_start: i32 = raw_show[bracket_open + 1..dash].parse().unwrap();
476
+ let year_end: i32 = raw_show[dash + 1..bracket_close].parse().unwrap();
477
+
478
+ (name, year_start, year_end)
479
+ }
480
+
481
+ // 正常ケース
482
+ parse_show_unsafe("Breaking Bad (2008-2013)"); // OK
483
+
484
+ // 異常ケース → panic!
485
+ parse_show_unsafe("Chernobyl (2019)"); // パニック! '-' がない
486
+ parse_show_unsafe("The Wire 2002-2008"); // パニック! '(' がない
487
+ ```
488
+
489
+ 小さな関数が `Option` を返すようにして、`?` 演算子で合成する。`?` は `None` なら即座に関数から `None` を返す。
490
+
491
+ ```rust
492
+ /// 番組名を抽出
493
+ pub fn extract_name(raw_show: &str) -> Option<String> {
494
+ let bracket_open = raw_show.find('(')?; // ? 演算子で None を伝播
495
+ if bracket_open > 0 {
496
+ Some(raw_show[..bracket_open].trim().to_string())
497
+ } else {
498
+ None
499
+ }
500
+ }
501
+ ```
502
+
503
+ ```rust
504
+ /// TV番組をパース(Option 版)
505
+ pub fn parse_show(raw_show: &str) -> Option<TvShow> {
506
+ let name = extract_name(raw_show)?;
507
+ let year_start = extract_year_start(raw_show)
508
+ .or_else(|| extract_single_year(raw_show))?;
509
+ let year_end = extract_year_end(raw_show)
510
+ .or_else(|| extract_single_year(raw_show))?;
511
+
512
+ Some(TvShow::new(&name, year_start, year_end))
513
+ }
514
+
515
+ // 正常ケース
516
+ parse_show("Breaking Bad (2008-2013)"); // Some(TvShow { ... })
517
+ parse_show("Chernobyl (2019)"); // Some(TvShow { ... })
518
+
519
+ // 異常ケース → None が返される(パニックなし)
520
+ parse_show("The Wire 2002-2008"); // None
521
+ ```
522
+
523
+ `or` と `or_else` は「`None` なら代替を使う」フォールバックである。
524
+
525
+ ```rust
526
+ let seven: Option<i32> = Some(7);
527
+ let eight: Option<i32> = Some(8);
528
+ let none: Option<i32> = None;
529
+
530
+ assert_eq!(seven.or(eight), Some(7)); // 最初が Some なのでそのまま
531
+ assert_eq!(none.or(eight), Some(8)); // 最初が None なので代替を使用
532
+ assert_eq!(seven.or(none), Some(7));
533
+ assert_eq!(none.or(none), None);
534
+ ```
535
+
536
+ 主要なメソッドは次のとおりである。
537
+
538
+ ```rust
539
+ let year: Option<i32> = Some(996);
540
+ let no_year: Option<i32> = None;
541
+
542
+ // map
543
+ assert_eq!(year.map(|y| y * 2), Some(1992));
544
+ assert_eq!(no_year.map(|y| y * 2), None);
545
+
546
+ // and_then (Scala の flatMap 相当)
547
+ assert_eq!(year.and_then(|y| Some(y * 2)), Some(1992));
548
+ assert_eq!(no_year.and_then(|y| Some(y * 2)), None);
549
+
550
+ // filter
551
+ assert_eq!(year.filter(|&y| y < 2020), Some(996));
552
+ assert_eq!(year.filter(|&y| y > 2020), None);
553
+
554
+ // unwrap_or
555
+ assert_eq!(year.unwrap_or(0), 996);
556
+ assert_eq!(no_year.unwrap_or(0), 0);
557
+ ```
558
+
559
+ ### 3.2 Result: 失敗の理由を持つ
560
+
561
+ `Option` は「なぜ失敗したか」を運べない。理由を持たせるときは `Result<T, E>` を使う。`Ok` が成功、`Err` が失敗である。`Option` から変えるには `ok_or` を使う。
562
+
563
+ ```rust
564
+ let year: Option<i32> = Some(996);
565
+ let no_year: Option<i32> = None;
566
+
567
+ assert_eq!(year.ok_or("no year given"), Ok(996));
568
+ assert_eq!(no_year.ok_or("no year given"), Err("no year given"));
569
+ ```
570
+
571
+ ```rust
572
+ pub fn extract_name(raw_show: &str) -> Result<String, String> {
573
+ let bracket_open = raw_show
574
+ .find('(')
575
+ .ok_or_else(|| format!("Can't find '(' in {}", raw_show))?;
576
+ if bracket_open > 0 {
577
+ Ok(raw_show[..bracket_open].trim().to_string())
578
+ } else {
579
+ Err(format!("Can't extract name from {}", raw_show))
580
+ }
581
+ }
582
+
583
+ extract_name("The Wire (2002-2008)"); // Ok("The Wire")
584
+ extract_name("(2022)"); // Err("Can't extract name from (2022)")
585
+ ```
586
+
587
+ `?` は `Result` でも同じように使える。`Err` なら即座に関数から `Err` を返す。
588
+
589
+ ```rust
590
+ pub fn parse_show(raw_show: &str) -> Result<TvShow, String> {
591
+ let name = extract_name(raw_show)?;
592
+ let year_start = extract_year_start(raw_show)
593
+ .or_else(|_| extract_single_year(raw_show))?;
594
+ let year_end = extract_year_end(raw_show)
595
+ .or_else(|_| extract_single_year(raw_show))?;
596
+
597
+ Ok(TvShow::new(&name, year_start, year_end))
598
+ }
599
+
600
+ parse_show("The Wire (2002-2008)"); // Ok(TvShow { ... })
601
+ parse_show("Mad Men ()"); // Err("Can't extract single year from ...")
602
+ parse_show("(2002-2008)"); // Err("Can't extract name from ...")
603
+ ```
604
+
605
+ ```rust
606
+ let year: Result<i32, &str> = Ok(996);
607
+ let no_year: Result<i32, &str> = Err("no year");
608
+
609
+ // map
610
+ assert_eq!(year.map(|y| y * 2), Ok(1992));
611
+ assert_eq!(no_year.map(|y| y * 2), Err("no year"));
612
+
613
+ // and_then
614
+ assert_eq!(year.and_then(|y| Ok(y * 2)), Ok(1992));
615
+ assert_eq!(no_year.and_then(|y| Ok(y * 2)), Err("no year"));
616
+
617
+ // ok (Option に変換)
618
+ assert_eq!(year.ok(), Some(996));
619
+ assert_eq!(no_year.ok(), None);
620
+ ```
621
+
622
+ ### 3.3 エラーの型を設計する
623
+
624
+ `String` のエラーを、業務の言葉の `enum` にする。Zettai 版は、`Option` の `None` と、状態遷移の拒否を、1 つのエラー型にまとめた。
625
+
626
+ ```rust
627
+ pub enum ZettaiError {
628
+ /// 同じ名前のリストがもうある。
629
+ ListAlreadyExists { list_name: ListName },
630
+ /// リストが見つからない。**第 6 章の `ListDoesNotExist` と、
631
+ /// `Option` の `None` が、ここで 1 つになった。**
632
+ ListNotFound { user: User, list_name: ListName },
633
+ /// 説明が空。
634
+ EmptyDescription,
635
+ }
636
+ ```
637
+
638
+ `?` は、エラーの型が違うときに `From` トレイトで変換する。変換を `impl From` で書いておけば、層をまたぐエラーも `?` だけで伝播できる。
639
+
640
+ ```rust
641
+ impl From<DomainError> for HttpError {
642
+ fn from(e: DomainError) -> Self {
643
+ HttpError::Domain(e)
644
+ }
645
+ }
646
+ ```
647
+
648
+ 境界で、エラーを HTTP の応答に対応付ける。`match` が網羅されるので、エラーの種類を足すと、対応付けを忘れた箇所がコンパイルエラーになる。
649
+
650
+ ```rust
651
+ fn failed(reason: ZettaiError) -> Reply {
652
+ match reason {
653
+ ZettaiError::ListNotFound { .. } => not_found(),
654
+ ZettaiError::ListAlreadyExists { .. } => Reply {
655
+ status: 409,
656
+ body: "<html><body><h1>409</h1></body></html>".to_string(),
657
+ },
658
+ ZettaiError::EmptyDescription => bad_request(),
659
+ }
660
+ }
661
+ ```
662
+
663
+ ### 3.4 リストの失敗の扱い
664
+
665
+ リストを処理するときの失敗の扱いは、次の戦略から選ぶ。`collect` は、結果の型で動作が変わる。
666
+
667
+ ```rust
668
+ pub fn parse_shows_best_effort(raw_shows: &[&str]) -> Vec<TvShow> {
669
+ raw_shows
670
+ .iter()
671
+ .filter_map(|raw| parse_show(raw)) // Some のみ収集
672
+ .collect()
673
+ }
674
+
675
+ pub fn parse_shows_all_or_nothing(raw_shows: &[&str]) -> Option<Vec<TvShow>> {
676
+ raw_shows.iter().map(|raw| parse_show(raw)).collect()
677
+ }
678
+
679
+ // 全部成功 → Some(vec![...])
680
+ parse_shows_all_or_nothing(&["Breaking Bad (2008-2013)", "Mad Men (2007-2015)"]);
681
+ // Some(vec![TvShow(...), TvShow(...)])
682
+
683
+ // 一つでも失敗 → None
684
+ parse_shows_all_or_nothing(&["Breaking Bad (2008-2013)", "Invalid"]);
685
+ // None
686
+ ```
687
+
688
+ `Vec<Option<T>>` を `Option<Vec<T>>` に畳む `collect` は、最初の `None` で止まる(all-or-nothing)。`Result` でも同じで、最初の `Err` で止まる。全部のエラーを集めたいときは、6.2 節の `Validated` を使う。
689
+
690
+ ### 3.5 enum で ADT を作る
691
+
692
+ 取りうる場合を限定した型(代数的データ型)を `enum` で表し、`match` で網羅的に処理する。場合を追加したとき、扱いを忘れている `match` はコンパイルエラーになる。
693
+
694
+ ```rust
695
+ /// 音楽ジャンル
696
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
697
+ pub enum MusicGenre {
698
+ HeavyMetal,
699
+ Pop,
700
+ HardRock,
701
+ Jazz,
702
+ }
703
+
704
+ /// 活動期間
705
+ #[derive(Debug, Clone, PartialEq)]
706
+ pub enum YearsActive {
707
+ /// 現在も活動中
708
+ StillActive { since: i32 },
709
+ /// 活動終了
710
+ ActiveBetween { start: i32, end: i32 },
711
+ }
712
+ ```
713
+
714
+ ```rust
715
+ /// アーティスト
716
+ #[derive(Debug, Clone, PartialEq)]
717
+ pub struct Artist {
718
+ pub name: String,
719
+ pub genre: MusicGenre,
720
+ pub origin: String,
721
+ pub years_active: YearsActive,
722
+ }
723
+ ```
724
+
725
+ ```rust
726
+ /// アーティストが指定期間に活動していたか判定
727
+ pub fn was_artist_active(artist: &Artist, year_start: i32, year_end: i32) -> bool {
728
+ match &artist.years_active {
729
+ YearsActive::StillActive { since } => *since <= year_end,
730
+ YearsActive::ActiveBetween { start, end } => *start <= year_end && *end >= year_start,
731
+ }
732
+ }
733
+
734
+ /// アーティストの活動年数を計算
735
+ pub fn active_length(artist: &Artist, current_year: i32) -> i32 {
736
+ match &artist.years_active {
737
+ YearsActive::StillActive { since } => current_year - since,
738
+ YearsActive::ActiveBetween { start, end } => end - start,
739
+ }
740
+ }
741
+ ```
742
+
743
+ `match` の網羅性は、コンパイラが検査する。ただし、ワイルドカード(`_`)を足すと網羅の検査が止まる。ワイルドカードを避ける規約は、第 9 章の lint で守る。
744
+
745
+ ### 3.6 使い分け
746
+
747
+ | 状況 | 使うもの |
748
+ | --- | --- |
749
+ | 値がないことだけを表したい | `Option<T>` と `?`、`or_else` |
750
+ | 失敗の理由を呼び出し側に伝えたい | `Result<T, E>` と `?` |
751
+ | `Option` を `Result` に変えたい | `ok_or`、`ok_or_else` |
752
+ | エラーの型を層ごとに変換したい | `impl From<E1> for E2` と `?` |
753
+ | すべての検証エラーをまとめて返したい | 6.2 節の `Validated` |
754
+ | 取りうる場合を限定して漏れを防ぎたい | `enum` と `match`(ワイルドカードを避ける) |
755
+ | 失敗を無視したくない | `Result` は `#[must_use]`。捨てるとコンパイラが警告する |
756
+
757
+ ---
758
+
759
+ ## 4. 副作用の管理
760
+
761
+ ### 4.1 副作用を値として扱う
762
+
763
+ 副作用のある処理を「実行しない記述」として値にすると、いつ実行するかを呼び出し側が決められる。まず、副作用を持つ不純な関数を示す。
764
+
765
+ ```rust
766
+ /// 不純な関数(副作用あり)- サイコロを振る
767
+ pub fn cast_the_die_impure() -> i32 {
768
+ use std::time::{SystemTime, UNIX_EPOCH};
769
+ let nanos = SystemTime::now()
770
+ .duration_since(UNIX_EPOCH)
771
+ .unwrap()
772
+ .subsec_nanos();
773
+ (nanos % 6) as i32 + 1
774
+ }
775
+ ```
776
+
777
+ ### 4.2 async は副作用の記述になる
778
+
779
+ Rust の `async fn` は、呼び出しただけでは本体が実行されない。`Future` という実行前の記述を返し、`.await` したときに初めて実行される。副作用を持つ処理の呼び出し位置が、`async` の文脈に限定される。
780
+
781
+ ```rust
782
+ /// 非同期でサイコロを振る(副作用を遅延)
783
+ pub async fn cast_the_die() -> i32 {
784
+ cast_the_die_impure()
785
+ }
786
+
787
+ /// 2回サイコロを振って合計を返す
788
+ pub async fn cast_the_die_twice() -> i32 {
789
+ let first = cast_the_die().await;
790
+ let second = cast_the_die().await;
791
+ first + second
792
+ }
793
+ ```
794
+
795
+ 合成は、`flat_map` ではなく、普通の逐次コードで書ける。Kotlin の `suspend` に近い役割である。
796
+
797
+ ただし Zettai 版は、`async` を採らず、同期のコードで 13 章を書いた(ADR-024)。1 プロセスで 1 リクエストずつ扱う規模なら、同期で足りた。「同期のほうが良かった」のではなく、「困らなかった」という結論である。この規模で分かったのは、「同期で書ける」ことだけである。
798
+
799
+ ### 4.3 純粋な計算と非同期処理を分ける
800
+
801
+ 純粋な計算と副作用を分け、副作用は関数として外から受け取る。ミーティングの空き時間の計算は純粋関数で、カレンダーの取得だけが非同期である。
802
+
803
+ ```rust
804
+ /// ミーティング時間を表す構造体
805
+ #[derive(Debug, Clone, PartialEq)]
806
+ pub struct MeetingTime {
807
+ pub start_hour: i32,
808
+ pub end_hour: i32,
809
+ }
810
+
811
+ /// 2つのミーティングが重なっているか判定(純粋関数)
812
+ pub fn meetings_overlap(m1: &MeetingTime, m2: &MeetingTime) -> bool {
813
+ m1.start_hour < m2.end_hour && m2.start_hour < m1.end_hour
814
+ }
815
+ ```
816
+
817
+ ### 4.4 失敗とリトライ
818
+
819
+ 失敗する非同期処理は `Result` を返す。フォールバックは `unwrap_or_else` で書き、再実行は汎用の関数にする。
820
+
821
+ ```rust
822
+ /// 失敗する可能性のあるカレンダー取得
823
+ pub async fn calendar_entries_may_fail(name: &str) -> Result<Vec<MeetingTime>, String> {
824
+ if name == "Error" {
825
+ Err("API call failed".to_string())
826
+ } else {
827
+ Ok(calendar_entries(name).await)
828
+ }
829
+ }
830
+
831
+ /// フォールバック付きのカレンダー取得
832
+ pub async fn calendar_entries_with_fallback(name: &str) -> Vec<MeetingTime> {
833
+ calendar_entries_may_fail(name)
834
+ .await
835
+ .unwrap_or_else(|_| vec![])
836
+ }
837
+ ```
838
+
839
+ ```rust
840
+ /// 指定回数リトライする
841
+ pub async fn retry<T, E, F, Fut>(action: F, max_retries: usize) -> Result<T, E>
842
+ where
843
+ F: Fn() -> Fut,
844
+ Fut: Future<Output = Result<T, E>>,
845
+ {
846
+ let mut last_error = None;
847
+ for _ in 0..max_retries {
848
+ match action().await {
849
+ Ok(result) => return Ok(result),
850
+ Err(e) => last_error = Some(e),
851
+ }
852
+ }
853
+ Err(last_error.unwrap())
854
+ }
855
+ ```
856
+
857
+ ### 4.5 複数の非同期処理を合成する
858
+
859
+ 独立した複数の非同期処理は、`join_all` で並行に実行できる。
860
+
861
+ ```rust
862
+ /// 複数人の予定を並行取得
863
+ pub async fn scheduled_meetings_for_all(attendees: &[&str]) -> Vec<MeetingTime> {
864
+ let futures: Vec<_> = attendees
865
+ .iter()
866
+ .map(|name| calendar_entries(name))
867
+ .collect();
868
+
869
+ let results = futures::future::join_all(futures).await;
870
+ results.into_iter().flatten().collect()
871
+ }
872
+ ```
873
+
874
+ 条件によって異なる `Future` を返すには、型を揃える `BoxFuture` を使う。
875
+
876
+ ```rust
877
+ pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
878
+
879
+ /// 条件に応じて異なる非同期処理を返す
880
+ pub fn conditional_async(condition: bool) -> BoxFuture<'static, &'static str> {
881
+ if condition {
882
+ Box::pin(async { "Success" })
883
+ } else {
884
+ Box::pin(async { "Failure" })
885
+ }
886
+ }
887
+ ```
888
+
889
+ ### 4.6 ストリーム: Iterator と Stream
890
+
891
+ 連続したデータを遅延評価で扱う。必要な要素だけが評価されるので、無限のストリームも定義できる。
892
+
893
+ | 型 | 評価 | 副作用(async) | 向く場面 |
894
+ | --- | --- | --- | --- |
895
+ | `Iterator` | 遅延・同期 | 呼び出せない | メモリ上の変換、無限列の定義 |
896
+ | `Stream`(futures) | 遅延・非同期 | 呼び出せる | I/O を伴う連続した値 |
897
+
898
+ ```rust
899
+ use futures::stream::{self, Stream, StreamExt};
900
+
901
+ /// イテレータからストリームを作成
902
+ pub fn numbers_stream(start: i32, end: i32) -> impl Stream<Item = i32> {
903
+ stream::iter(start..=end)
904
+ }
905
+ ```
906
+
907
+ ```rust
908
+ /// 2つのストリームを結合
909
+ pub fn concat_streams<S1, S2, T>(stream1: S1, stream2: S2) -> impl Stream<Item = T>
910
+ where
911
+ S1: Stream<Item = T>,
912
+ S2: Stream<Item = T>,
913
+ {
914
+ stream1.chain(stream2)
915
+ }
916
+ ```
917
+
918
+ 無限のストリームは `unfold` で定義する。
919
+
920
+ ```rust
921
+ /// 無限ストリームを生成
922
+ pub fn infinite_stream<T, F>(initial: T, f: F) -> impl Stream<Item = T>
923
+ where
924
+ T: Clone,
925
+ F: FnMut(&T) -> T,
926
+ {
927
+ stream::unfold((initial, f), |(state, mut f)| async move {
928
+ let next = f(&state);
929
+ Some((state, (next, f)))
930
+ })
931
+ }
932
+
933
+ /// フィボナッチ数列のストリーム
934
+ pub fn fibonacci_stream() -> impl Stream<Item = u64> {
935
+ stream::unfold((0u64, 1u64), |(a, b)| async move {
936
+ Some((a, (b, a + b)))
937
+ })
938
+ }
939
+ ```
940
+
941
+ ```rust
942
+ let stream = fibonacci_stream();
943
+ let first_ten: Vec<u64> = stream.take(10).collect().await;
944
+ assert_eq!(first_ten, vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
945
+ ```
946
+
947
+ ---
948
+
949
+ ## 5. 並行処理
950
+
951
+ ### 5.1 共有状態は所有権で守る
952
+
953
+ Rust は、複数のスレッドから共有する値に、`Send` と `Sync` の条件を課す。条件を満たさない型を共有しようとすると、コンパイルが止まる。共有する状態は `Arc` で所有を共有し、`Mutex` で書き込みを直列にする。
954
+
955
+ ```rust
956
+ use std::sync::Arc;
957
+ use tokio::sync::Mutex;
958
+
959
+ /// アトミックカウンター
960
+ #[derive(Clone)]
961
+ pub struct AtomicCounter {
962
+ value: Arc<Mutex<i32>>,
963
+ }
964
+
965
+ impl AtomicCounter {
966
+ pub fn new(initial: i32) -> Self {
967
+ Self {
968
+ value: Arc::new(Mutex::new(initial)),
969
+ }
970
+ }
971
+
972
+ pub async fn get(&self) -> i32 {
973
+ *self.value.lock().await
974
+ }
975
+
976
+ pub async fn increment(&self) {
977
+ let mut guard = self.value.lock().await;
978
+ *guard += 1;
979
+ }
980
+
981
+ pub async fn update<F>(&self, f: F)
982
+ where
983
+ F: FnOnce(i32) -> i32,
984
+ {
985
+ let mut guard = self.value.lock().await;
986
+ *guard = f(*guard);
987
+ }
988
+ }
989
+ ```
990
+
991
+ 読み取りが多いなら `RwLock` で読み書きを分ける。ロックのガードは、スコープを抜けると自動で解放される。
992
+
993
+ ### 5.2 並列実行とキャンセル
994
+
995
+ `tokio::spawn` で別のタスクを起動し、`JoinHandle` で結果を待つ。タスクのキャンセルは `abort` である。
996
+
997
+ ```rust
998
+ pub async fn run_parallel<T, U, F, Fut>(items: Vec<T>, f: F) -> Vec<U>
999
+ where
1000
+ T: Send + 'static,
1001
+ U: Send + 'static,
1002
+ F: Fn(T) -> Fut + Send + Sync + 'static,
1003
+ Fut: std::future::Future<Output = U> + Send,
1004
+ {
1005
+ let f = Arc::new(f);
1006
+ let handles: Vec<JoinHandle<U>> = items
1007
+ .into_iter()
1008
+ .map(|item| {
1009
+ let f = Arc::clone(&f);
1010
+ tokio::spawn(async move { f(item).await })
1011
+ })
1012
+ .collect();
1013
+
1014
+ let mut results = Vec::with_capacity(handles.len());
1015
+ for handle in handles {
1016
+ results.push(handle.await.unwrap());
1017
+ }
1018
+ results
1019
+ }
1020
+ ```
1021
+
1022
+ `join_all`(4.5 節)は、起動した `Future` を同じタスクの中で並行に進める。`tokio::spawn` は別のタスクとして並列に走らせる。
1023
+
1024
+ ### 5.3 チャネルで状態を共有しない
1025
+
1026
+ 共有状態をロックで守る代わりに、値をチャネルで送る方法がある。状態を 1 つのタスクだけが持てば、ロックは要らない。
1027
+
1028
+ | チャネル | 通信の形 |
1029
+ | --- | --- |
1030
+ | `mpsc` | 複数の送信者から、単一の受信者へ |
1031
+ | `oneshot` | 一回限りの通信 |
1032
+ | `broadcast` | 複数の受信者へ同じ値を配る |
1033
+
1034
+ ```rust
1035
+ use tokio::sync::mpsc;
1036
+
1037
+ pub async fn basic_mpsc_example() -> Vec<i32> {
1038
+ let (tx, mut rx) = mpsc::channel(10);
1039
+
1040
+ // 送信タスク
1041
+ tokio::spawn(async move {
1042
+ for i in 1..=3 {
1043
+ tx.send(i).await.unwrap();
1044
+ }
1045
+ });
1046
+
1047
+ // 受信
1048
+ let mut results = Vec::new();
1049
+ while let Some(value) = rx.recv().await {
1050
+ results.push(value);
1051
+ }
1052
+
1053
+ results
1054
+ }
1055
+ ```
1056
+
1057
+ ```rust
1058
+ use tokio::sync::oneshot;
1059
+
1060
+ pub async fn compute_with_oneshot(input: i32) -> i32 {
1061
+ let (tx, rx) = oneshot::channel();
1062
+
1063
+ tokio::spawn(async move {
1064
+ let result = input * input;
1065
+ tx.send(result).unwrap();
1066
+ });
1067
+
1068
+ rx.await.unwrap()
1069
+ }
1070
+ ```
1071
+
1072
+ ### 5.4 アクターで状態を閉じ込める
1073
+
1074
+ 状態を 1 つのタスクに閉じ込め、メッセージで操作するパターンがアクターである。メッセージは `enum` で表し、応答が要るときは `oneshot::Sender` を同封する。
1075
+
1076
+ ```rust
1077
+ pub enum CounterMessage {
1078
+ Increment,
1079
+ Decrement,
1080
+ Get(oneshot::Sender<i32>),
1081
+ Stop,
1082
+ }
1083
+ ```
1084
+
1085
+ ```rust
1086
+ pub enum BankMessage {
1087
+ Deposit(i32),
1088
+ Withdraw(i32, oneshot::Sender<Result<(), String>>),
1089
+ GetBalance(oneshot::Sender<i32>),
1090
+ Stop,
1091
+ }
1092
+ ```
1093
+
1094
+ イベントを多くの購読者に配るイベントバスも、`broadcast` で作れる。
1095
+
1096
+ ```rust
1097
+ #[derive(Debug, Clone)]
1098
+ pub enum Event {
1099
+ UserLoggedIn { user_id: String },
1100
+ UserLoggedOut { user_id: String },
1101
+ MessageSent { from: String, to: String, content: String },
1102
+ }
1103
+ ```
1104
+
1105
+ ---
1106
+
1107
+ ## 6. 代数構造の実用
1108
+
1109
+ モノイド・ファンクタ・モナド・アプリカティブ・プロファンクタは、これまでに使ってきた `fold`、`map`、`and_then`、`zip` の共通のパターンに付けた名前である。数学の用語を覚えることが目的ではなく、「この型にこの操作が使える」と見抜いて、合成の道具を選べるようにするために使う。
1110
+
1111
+ Zettai 版では、5 つの構造がすべて現れた。言語が与えたのは 2 つだけで、残りは自作した。
1112
+
1113
+ | 構造 | 章 | Rust |
1114
+ | --- | --- | --- |
1115
+ | モノイド | 5 | 自作 |
1116
+ | ファンクタ | 7・8 | 言語にある(`Result` / `Option` の `map`) |
1117
+ | モナド | 9 | 言語にある(`and_then` と `?`) |
1118
+ | アプリカティブ | 11 | 自作(`Validated`) |
1119
+ | プロファンクタ | 12 | 自作(`Converter`) |
1120
+
1121
+ ### 6.1 モノイドでイベントを畳み込む
1122
+
1123
+ 状態を変える操作を「リストから新しいリストへの変換」として値にし、合成(`compose`)と単位元(`identity`)を用意する。これでイベントの列を `fold` で 1 つの変換に畳める。
1124
+
1125
+ ```rust
1126
+ #[derive(Debug, Clone, PartialEq, Eq)]
1127
+ pub enum ToDoListEvent {
1128
+ ListCreated { list_name: ListName },
1129
+ ItemAdded { item: ToDoItem },
1130
+ }
1131
+ ```
1132
+
1133
+ ```rust
1134
+ pub fn transform_for(event: ToDoListEvent) -> Transform {
1135
+ match event {
1136
+ ToDoListEvent::ListCreated { list_name } => Box::new(move |_previous| ToDoList {
1137
+ list_name: list_name.clone(),
1138
+ items: Vec::new(),
1139
+ }),
1140
+ ToDoListEvent::ItemAdded { item } => Box::new(move |list: ToDoList| ToDoList {
1141
+ list_name: list.list_name,
1142
+ items: [list.items, vec![item.clone()]].concat(),
1143
+ }),
1144
+ }
1145
+ }
1146
+ ```
1147
+
1148
+ ```rust
1149
+ /// 何もしない変換。**合成の単位元。**
1150
+ pub fn identity() -> Transform {
1151
+ Box::new(|list| list)
1152
+ }
1153
+
1154
+ /// 2 つの変換を繋ぐ。**結果も変換なので、繰り返せる。**
1155
+ pub fn compose(f: Transform, g: Transform) -> Transform {
1156
+ Box::new(move |list| g(f(list)))
1157
+ }
1158
+ ```
1159
+
1160
+ ```rust
1161
+ pub fn fold_events(events: Vec<ToDoListEvent>) -> Transform {
1162
+ events
1163
+ .into_iter()
1164
+ .map(transform_for)
1165
+ .fold(identity(), compose)
1166
+ }
1167
+ ```
1168
+
1169
+ ```rust
1170
+ pub fn replay(events: Vec<ToDoListEvent>) -> ToDoList {
1171
+ fold_events(events)(empty_list())
1172
+ }
1173
+ ```
1174
+
1175
+ 合成を型(`impl Fn`)で表すと、`fold` で畳もうとしたときに型が合わず、コンパイルが止まる(E0308)。`Box<dyn Fn>` の値にして名前(`Transform`)を付けると、モノイドの形がそのまま出る。
1176
+
1177
+ ```rust
1178
+ type Transform = Box<dyn Fn(Vec<String>) -> Vec<String>>;
1179
+
1180
+ fn make(prefix: &str) -> Transform {
1181
+ Box::new(move |mut s| { s.push(prefix.to_string()); s })
1182
+ }
1183
+ ```
1184
+
1185
+ ### 6.2 ファンクタとモナドは言語にある
1186
+
1187
+ `Option` と `Result` の `map` はファンクタ、`and_then` と `?` はモナドである。自作の型にも、同じ形の `map` を用意すると、射影のクエリで同じように使える。
1188
+
1189
+ ```rust
1190
+ pub fn summary_transform_for(event: ToDoListEvent) -> SummaryTransform {
1191
+ match event {
1192
+ ToDoListEvent::ListCreated { list_name } => Box::new(move |_previous| ListSummary {
1193
+ list_name: list_name.0.clone(),
1194
+ item_count: 0,
1195
+ }),
1196
+ ToDoListEvent::ItemAdded { .. } => Box::new(|summary| ListSummary {
1197
+ item_count: summary.item_count + 1,
1198
+ ..summary
1199
+ }),
1200
+ }
1201
+ }
1202
+ ```
1203
+
1204
+ ```rust
1205
+ pub fn map(self, f: impl Fn(ListSummary) -> ListSummary) -> Summaries {
1206
+ Summaries(self.0.into_iter().map(f).collect())
1207
+ }
1208
+ ```
1209
+
1210
+ モナドは、前の結果を使って次を決める。永続化では、読む、検証する、追記する、を `?` で繋ぐ。
1211
+
1212
+ ```rust
1213
+ pub fn handle_with_store(
1214
+ store: &dyn EventStore,
1215
+ user: &User,
1216
+ list_name: &ListName,
1217
+ command: crate::ToDoListCommand,
1218
+ ) -> Result<crate::ToDoList, ZettaiError> {
1219
+ let before = store.events_of(user, list_name)?;
1220
+ let new_events = crate::execute(user, list_name, &before, command)?;
1221
+ store.append(user, list_name, &new_events)?;
1222
+ let all = [before, new_events].concat();
1223
+ Ok(crate::replay(all))
1224
+ }
1225
+ ```
1226
+
1227
+ ### 6.3 アプリカティブでバリデーションを合成する
1228
+
1229
+ `Result` のモナドは、最初のエラーで止まる。独立した検証をすべて走らせ、エラーを全部集めるには、`Validated` を自作する。`zip` は、両方がだめなら両方の理由を返す。
1230
+
1231
+ ```rust
1232
+ pub fn zip<U>(self, other: Validated<U>) -> Validated<(T, U)> {
1233
+ match (self, other) {
1234
+ (Validated::Valid(a), Validated::Valid(b)) => Validated::Valid((a, b)),
1235
+ // **両方だめなら両方返す。** ここがこの型の存在理由
1236
+ (Validated::Invalid(a), Validated::Invalid(b)) => Validated::Invalid([a, b].concat()),
1237
+ (Validated::Invalid(a), _) => Validated::Invalid(a),
1238
+ (_, Validated::Invalid(b)) => Validated::Invalid(b),
1239
+ }
1240
+ }
1241
+ ```
1242
+
1243
+ ```rust
1244
+ pub fn valid_new_name(name: &str) -> Validated<crate::ListName> {
1245
+ not_empty(name)
1246
+ .zip(within_limit(name))
1247
+ .zip(without_markup(name))
1248
+ .map(|_| crate::ListName::new(name))
1249
+ }
1250
+ ```
1251
+
1252
+ ### 6.4 プロファンクタで入力と出力の両方を変換する
1253
+
1254
+ JSON の変換のように、書き込みと読み込みの両方向を持つ型は、入力側を `contramap` で変換する。
1255
+
1256
+ ```rust
1257
+ pub type Write<A, B> = Box<dyn Fn(&A) -> B>;
1258
+ pub type Read<A, B> = Box<dyn Fn(&B) -> Result<A, ZettaiError>>;
1259
+ ```
1260
+
1261
+ ```rust
1262
+ pub struct Converter<A, B> {
1263
+ write: Write<A, B>,
1264
+ read: Read<A, B>,
1265
+ }
1266
+ ```
1267
+
1268
+ ```rust
1269
+ pub fn contramap<A2: 'static>(
1270
+ self,
1271
+ to: impl Fn(&A2) -> A + 'static,
1272
+ from: impl Fn(A) -> A2 + 'static,
1273
+ ) -> Converter<A2, B> {
1274
+ Converter {
1275
+ write: Box::new(move |a2| (self.write)(&to(a2))),
1276
+ read: Box::new(move |b| (self.read)(b).map(&from)),
1277
+ }
1278
+ }
1279
+ ```
1280
+
1281
+ ### 6.5 どの構造を選ぶか: 判断の手順
1282
+
1283
+ 5 つの構造を「いつ選ぶか」で整理する。
1284
+
1285
+ | 構造 | 持つもの | 選ぶとき | 連載の章 |
1286
+ | --- | --- | --- | --- |
1287
+ | モノイド | 合成と単位元 | 同じ型のものを畳み込みたい | 5 |
1288
+ | ファンクタ | `map` | 箱の中身だけを変えたい | 7・8 |
1289
+ | アプリカティブ | `map` と `zip` | 独立な計算を合わせ、失敗を全部集めたい | 11 |
1290
+ | モナド | `map` と `and_then` と `pure` | 前の結果を使って次を決めたい | 9・10 |
1291
+ | プロファンクタ | `map` と `contramap` | 入力と出力の両方を変換したい | 12 |
1292
+
1293
+ 実際には、次の順で考えると決まる。
1294
+
1295
+ ```mermaid
1296
+ flowchart TD
1297
+ Q1{入力と出力の両方を<br>変換する?} -->|はい| P[プロファンクタ<br>map と contramap]
1298
+ Q1 -->|いいえ| Q2{前の結果を使って<br>次を決める?}
1299
+ Q2 -->|はい| M[モナド<br>and_then と ?]
1300
+ Q2 -->|いいえ| Q3{複数の結果を<br>合わせる?}
1301
+ Q3 -->|はい| Q4{失敗を全部<br>集めたい?}
1302
+ Q4 -->|はい| A[アプリカティブ<br>zip]
1303
+ Q4 -->|いいえ| M2[モナドでもよい]
1304
+ Q3 -->|いいえ| Q5{同じ型を<br>畳み込む?}
1305
+ Q5 -->|はい| MO[モノイド]
1306
+ Q5 -->|いいえ| F[ファンクタ<br>map]
1307
+ ```
1308
+
1309
+ 上から順に「いいえ」なら下へ進む。強い構造から確かめて、要らなければ弱いものにする。
1310
+
1311
+ 「強い」とは「できることが多い」という意味で、「偉い」という意味ではない。`map` で足りるなら `map` を使う。できることを増やすと、できなくなることがある。モナドは「前の結果を使う」能力を得た代わりに、「失敗を集める」能力を失った。必要な強さを選ぶ。
1312
+
1313
+ ---
1314
+
1315
+ ## 7. アーキテクチャ: 関数と型で組み立てる
1316
+
1317
+ ### 7.1 薄い縦串から始める
1318
+
1319
+ 最初に、HTTP からドメイン、表示までを貫く最小の経路を作る。Zettai 版は、HTTP のハンドラを「パスを受け取り、応答を返す」関数にした。サーバの型を返さないので、サーバを起こさずに関数として呼べる。
1320
+
1321
+ ```rust
1322
+ /// 応答。状態コードと本文だけを持つ。
1323
+ ///
1324
+ /// `tiny_http` の型を返さないのは、**この関数を HTTP 無しでテストできる**
1325
+ /// ようにするため。サーバの組み立ては `src/bin/zettai.rs` が持つ。
1326
+ #[derive(Debug, PartialEq, Eq)]
1327
+ pub struct Reply {
1328
+ pub status: u16,
1329
+ pub body: String,
1330
+ }
1331
+ ```
1332
+
1333
+ ```rust
1334
+ #[test]
1335
+ fn a_known_list_is_rendered() {
1336
+ let reply = handle("/todo/uberto/book");
1337
+ assert_eq!(reply.status, 200);
1338
+ assert!(reply.body.contains("<li>write chapter</li>"));
1339
+ assert!(reply.body.contains("<h1>book</h1>"));
1340
+ }
1341
+ ```
1342
+
1343
+ パスの分解は、スライスのパターンマッチで書ける。
1344
+
1345
+ ```rust
1346
+ let (user, list_name) = match parts.as_slice() {
1347
+ ["todo", user, list_name] => (User::new(user), ListName::new(list_name)),
1348
+ _ => return not_found(),
1349
+ };
1350
+ ```
1351
+
1352
+ ### 7.2 依存は関数の型とトレイトで表す
1353
+
1354
+ 外部とのやり取りを、関数の値(クロージャ)として受け取る。ハブは、取得と保存の関数を持つだけの型である。テストでは、渡す関数を差し替える。
1355
+
1356
+ ```rust
1357
+ pub struct ToDoListHub<F, S> {
1358
+ fetch: F,
1359
+ save: S,
1360
+ }
1361
+ ```
1362
+
1363
+ ```rust
1364
+ #[test]
1365
+ fn the_hub_uses_the_function_it_was_given() {
1366
+ // 埋め込みのデータではなく、**ここで渡したものが返る**。
1367
+ let hub = ToDoListHub::new(
1368
+ |_u, name| {
1369
+ Some(ToDoList {
1370
+ list_name: name.clone(),
1371
+ items: vec![ToDoItem::new("渡したほう")],
1372
+ })
1373
+ },
1374
+ |_u, _l| {},
1375
+ );
1376
+ ```
1377
+
1378
+ ハブのメソッドは、受け取ったリストを書き換えず、足した新しいリストを返す。
1379
+
1380
+ ```rust
1381
+ /// リストに項目を足す。**足した後のリストを返す。**
1382
+ ///
1383
+ /// 受け取ったリストを書き換えるのではなく、**足した新しいリストを作る**。
1384
+ /// もとのリストは変わらないので、第 5 章で「イベントを適用する関数」に
1385
+ /// そのまま化ける。
1386
+ pub fn add_item(&self, user: &User, list_name: &ListName, item: ToDoItem) -> Option<ToDoList> {
1387
+ let list = (self.fetch)(user, list_name)?;
1388
+ let updated = ToDoList {
1389
+ list_name: list.list_name,
1390
+ items: [list.items, vec![item]].concat(),
1391
+ };
1392
+ (self.save)(user, &updated);
1393
+ Some(updated)
1394
+ }
1395
+ ```
1396
+
1397
+ 同じシナリオを複数の経路(ドメインを直接、HTTP のハンドラを通す、データベース)に流すには、操作をトレイトにまとめて、経路ごとに実装する。
1398
+
1399
+ ```rust
1400
+ /// シナリオが使える操作。**経路ごとに実装する。**
1401
+ ///
1402
+ /// Kotlin 版は Pesticide の `DdtActions`、なでしこ3 版は関数値を詰めた辞書。
1403
+ /// Rust はトレイトがそのまま当たる。
1404
+ pub trait ZettaiActions {
1405
+ /// 記録に残す経路の名前。どの経路で落ちたかを知るために要る。
1406
+ fn route(&self) -> &'static str;
1407
+ /// 利用者のリストの項目を、説明の並びで返す。無ければ `None`。
1408
+ fn items_of(&self, user: &str, list_name: &str) -> Option<Vec<String>>;
1409
+ }
1410
+ ```
1411
+
1412
+ ```rust
1413
+ #[test]
1414
+ fn every_route_tells_the_same_story() {
1415
+ for actions in all_routes() {
1416
+ uberto_sees_his_book_list(actions.as_ref());
1417
+ an_empty_list_is_still_a_list(actions.as_ref());
1418
+ an_unknown_list_is_not_found(actions.as_ref());
1419
+ }
1420
+ }
1421
+ ```
1422
+
1423
+ ### 7.3 コマンドと状態遷移表
1424
+
1425
+ コマンド(意図)を `enum` で表し、拒否の理由も `enum` にする。状態は 3 つの場合に絞り、状態とコマンドの組で `match` する遷移表を作る。
1426
+
1427
+ ```rust
1428
+ #[derive(Debug, Clone, PartialEq, Eq)]
1429
+ pub enum ToDoListCommand {
1430
+ CreateList { list_name: ListName },
1431
+ AddItem { item: ToDoItem },
1432
+ }
1433
+ ```
1434
+
1435
+ ```rust
1436
+ #[derive(Debug, Clone, PartialEq, Eq)]
1437
+ pub enum Rejected {
1438
+ /// 同じ名前のリストがもうある。
1439
+ ListAlreadyExists,
1440
+ /// リストがまだ無い。
1441
+ ListDoesNotExist,
1442
+ }
1443
+ ```
1444
+
1445
+ ```rust
1446
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1447
+ pub enum ListState {
1448
+ Missing,
1449
+ Empty,
1450
+ HasItems,
1451
+ }
1452
+ ```
1453
+
1454
+ ```rust
1455
+ pub fn execute_in(
1456
+ state: ListState,
1457
+ command: ToDoListCommand,
1458
+ ) -> Result<Vec<ToDoListEvent>, Rejected> {
1459
+ match (state, command) {
1460
+ (ListState::Missing, ToDoListCommand::CreateList { list_name }) => {
1461
+ Ok(vec![ToDoListEvent::ListCreated { list_name }])
1462
+ }
1463
+ (ListState::Missing, ToDoListCommand::AddItem { .. }) => Err(Rejected::ListDoesNotExist),
1464
+ (ListState::Empty, ToDoListCommand::CreateList { .. }) => Err(Rejected::ListAlreadyExists),
1465
+ (ListState::Empty, ToDoListCommand::AddItem { item }) => {
1466
+ Ok(vec![ToDoListEvent::ItemAdded { item }])
1467
+ }
1468
+ (ListState::HasItems, ToDoListCommand::CreateList { .. }) => {
1469
+ Err(Rejected::ListAlreadyExists)
1470
+ }
1471
+ (ListState::HasItems, ToDoListCommand::AddItem { item }) => {
1472
+ Ok(vec![ToDoListEvent::ItemAdded { item }])
1473
+ }
1474
+ }
1475
+ }
1476
+ ```
1477
+
1478
+ ```rust
1479
+ pub fn state_of_list(list: Option<&ToDoList>) -> ListState {
1480
+ match list {
1481
+ None => ListState::Missing,
1482
+ Some(list) if list.items.is_empty() => ListState::Empty,
1483
+ Some(_) => ListState::HasItems,
1484
+ }
1485
+ }
1486
+ ```
1487
+
1488
+ マスが足りなければ、コンパイラが止める(E0004)。ただし、このコンパイラの検査が守る範囲は、思っているより狭い(9.4 節)。
1489
+
1490
+ ### 7.4 状態をイベントの列として扱う
1491
+
1492
+ 現在の状態を直接更新せず、起きた事実(イベント)を追記し、状態はイベントの再生で導く。再生は 6.1 節のモノイドの畳み込みである。読み取り用の表(射影)も、イベントの列から作る(6.2 節)。書き込み(コマンドからイベント)と読み取り(イベントから射影)を分ける設計が CQRS である。
1493
+
1494
+ ### 7.5 永続化とトランザクション境界
1495
+
1496
+ 永続化の窓口も、トレイトにする。PostgreSQL 版とインメモリ版を差し替えられる。
1497
+
1498
+ ```rust
1499
+ pub trait EventStore {
1500
+ /// 起きたことを追記する。
1501
+ fn append(
1502
+ &self,
1503
+ user: &User,
1504
+ list_name: &ListName,
1505
+ events: &[ToDoListEvent],
1506
+ ) -> Result<(), ZettaiError>;
1507
+
1508
+ /// 起きたことを順に読み出す。
1509
+ fn events_of(
1510
+ &self,
1511
+ user: &User,
1512
+ list_name: &ListName,
1513
+ ) -> Result<Vec<ToDoListEvent>, ZettaiError>;
1514
+ }
1515
+ ```
1516
+
1517
+ 複数の操作を 1 つのトランザクションにまとめるには、トランザクションを値として返すのではなく、区間として貸す。`commit` の呼び忘れが起こりえなくなる。
1518
+
1519
+ ```rust
1520
+ pub trait TransactionalStore {
1521
+ fn in_transaction(&self, work: UnitOfWork<'_>) -> Result<ToDoList, ZettaiError>;
1522
+ }
1523
+ ```
1524
+
1525
+ ```rust
1526
+ pub fn handle_in_transaction(
1527
+ store: &dyn TransactionalStore,
1528
+ user: &User,
1529
+ list_name: &ListName,
1530
+ command: crate::ToDoListCommand,
1531
+ ) -> Result<ToDoList, ZettaiError> {
1532
+ store.in_transaction(&|tx| {
1533
+ let before = tx.events_of(user, list_name)?;
1534
+ let new_events = crate::execute(user, list_name, &before, command.clone())?;
1535
+ tx.append(user, list_name, &new_events)?;
1536
+ Ok(crate::replay([before, new_events].concat()))
1537
+ })
1538
+ }
1539
+ ```
1540
+
1541
+ ### 7.6 バリデーションと境界
1542
+
1543
+ 境界で、生の文字列を検証済みの型に変え、内側では検証を繰り返さない。検証には 6.3 節の `Validated` を使う。
1544
+
1545
+ ### 7.7 監視は合成で足す
1546
+
1547
+ ログは、業務の言葉で書いた `Record` として扱う。
1548
+
1549
+ ```rust
1550
+ pub struct Record {
1551
+ /// 何が起きたか。業務の言葉で書く。
1552
+ pub what: String,
1553
+ /// 付帯する値。**キーと値の並び。**
1554
+ pub fields: Vec<(String, String)>,
1555
+ }
1556
+ ```
1557
+
1558
+ ### 7.8 全体の形と、クレートによる境界
1559
+
1560
+ ```mermaid
1561
+ flowchart LR
1562
+ A[HTTP<br>パス → Reply] --> B[入力の検証<br>Validated]
1563
+ B --> C[コマンド]
1564
+ C --> D[execute_in<br>状態 + コマンド → Result イベント列]
1565
+ D --> E[EventStore<br>トランザクション]
1566
+ E --> F[射影<br>fold]
1567
+ F --> G[HTML 表示]
1568
+ ```
1569
+
1570
+ 核になる純粋関数(検証、`execute_in`、`replay`、射影)を中心に置き、副作用(HTTP、データベース、ログ)は外側の薄い層に寄せる。
1571
+
1572
+ Zettai 版は、ドメインのクレートを、13 章通して依存 0 にした。ドメインを別のクレートに分けると、ドメインが HTTP やデータベースを呼ぶコードは、そもそもコンパイルできない。境界検査のコードを 1 行も書かずに済んだ。
1573
+
1574
+ ---
1575
+
1576
+ ## 8. テスト戦略
1577
+
1578
+ 純粋関数は、入力を与えて出力を確かめるだけでテストできる。副作用は関数やトレイトとして外から渡し、テスト用に差し替える。
1579
+
1580
+ ### 8.1 テストの土台は言語にある
1581
+
1582
+ `#[test]` と `assert_eq!` が言語にある。テストは、テスト対象と同じファイルの `#[cfg(test)]` モジュールに書ける。
1583
+
1584
+ ```rust
1585
+ #[cfg(test)]
1586
+ mod tests {
1587
+ use super::*;
1588
+
1589
+ #[test]
1590
+ fn test_word_score() {
1591
+ assert_eq!(word_score("Scala"), 3);
1592
+ assert_eq!(word_score("function"), 8);
1593
+ assert_eq!(word_score("aaa"), 0);
1594
+ assert_eq!(word_score(""), 0);
1595
+ }
1596
+ }
1597
+ ```
1598
+
1599
+ ### 8.2 テスト用スタブ
1600
+
1601
+ 外部への依存をトレイトで受け取り、テスト用の実装を差し替える。失敗のシミュレーションも、スタブに持たせる。
1602
+
1603
+ ```rust
1604
+ pub struct StubDataAccess {
1605
+ attractions: Vec<Attraction>,
1606
+ artists: HashMap<LocationId, Vec<MusicArtist>>,
1607
+ movies: HashMap<LocationId, Vec<Movie>>,
1608
+ artists_error: Option<String>,
1609
+ movies_error: Option<String>,
1610
+ }
1611
+ ```
1612
+
1613
+ ```rust
1614
+ #[tokio::test]
1615
+ async fn test_travel_guide_with_error() {
1616
+ let data = Arc::new(
1617
+ StubDataAccess::new()
1618
+ .with_attractions(vec![create_test_attraction()])
1619
+ .with_artists_error("Artist service unavailable")
1620
+ );
1621
+
1622
+ let guide = travel_guide(data.as_ref(), "Test").await;
1623
+
1624
+ assert!(guide.is_some());
1625
+ assert!(!guide.unwrap().search_report.errors.is_empty());
1626
+ }
1627
+ ```
1628
+
1629
+ ### 8.3 プロパティベーステスト
1630
+
1631
+ 具体的な値の例ではなく、「どんな入力でも成り立つ性質」をテストする。`proptest` で入力を生成する。
1632
+
1633
+ ```rust
1634
+ use proptest::prelude::*;
1635
+
1636
+ proptest! {
1637
+ /// ソートは要素を保持する
1638
+ #[test]
1639
+ fn sort_by_population_preserves_elements(
1640
+ populations in prop::collection::vec(0i32..10_000_000, 0..10)
1641
+ ) {
1642
+ let locations: Vec<Location> = populations
1643
+ .iter()
1644
+ .enumerate()
1645
+ .map(|(i, &pop)| Location::new(
1646
+ LocationId::new(&format!("loc{}", i)),
1647
+ &format!("City{}", i),
1648
+ pop,
1649
+ ))
1650
+ .collect();
1651
+
1652
+ let sorted = sort_by_population(locations.clone());
1653
+
1654
+ prop_assert_eq!(sorted.len(), locations.len());
1655
+ }
1656
+ }
1657
+ ```
1658
+
1659
+ なお Zettai 版は、`proptest` を入れず、20 行ほどの乱数生成を自作した(依存を +38、ビルドを +26.77 秒増やさないため)。道具は、実際に調べて決める(10.1 節)。
1660
+
1661
+ ### 8.4 代数構造の法則をテストする
1662
+
1663
+ 代数構造は、実装が法則を守っているかをテストで確かめる。たとえばファンクタの法則は、「2 回 `map` するのは、合成した関数を 1 回 `map` するのと同じ」である。
1664
+
1665
+ ```rust
1666
+ #[test]
1667
+ fn mapping_twice_equals_mapping_the_composition() {
1668
+ let mut rng = Rng::new(31415926);
1669
+ for _ in 0..TRIALS {
1670
+ let outcome = any_outcome(&mut rng);
1671
+ let twice = outcome.clone().map(add_marker).map(rename);
1672
+ let once = outcome.map(|list| rename(add_marker(list)));
1673
+ assert_eq!(twice, once);
1674
+ }
1675
+ }
1676
+ ```
1677
+
1678
+ モナドの結合則も同じ形で書ける。
1679
+
1680
+ ```rust
1681
+ #[test]
1682
+ fn associativity() {
1683
+ let mut rng = Rng::new(27182818);
1684
+ for _ in 0..TRIALS {
1685
+ let outcome = any_outcome(&mut rng);
1686
+ let left = outcome.clone().and_then(add_one).and_then(rename);
1687
+ let right = outcome.and_then(|list| add_one(list).and_then(rename));
1688
+ assert_eq!(left, right);
1689
+ }
1690
+ }
1691
+ ```
1692
+
1693
+ 法則のテストが本当に効いているかは、法則をわざと壊して、検出できるかで確かめる。Zettai 版は、壊したときの検出率が期待値の 3〜4σ 以内に入ることをしきい値にした。しきい値を 1.58σ に置いたなでしこ3 版では、CI が確率 5.7% で落ちた。
1694
+
1695
+ | 構造 | 壊し方 | 検出 | 期待値(σ) |
1696
+ | --- | --- | --- | --- |
1697
+ | モノイド | `item7` のときだけ足さない | 12 / 200 | 15(3.72) |
1698
+ | ファンクタ(`Result`) | 項目 2 件のとき飛ばす | 50 / 200 | 50(6.12) |
1699
+ | モナド | 項目 1 件のとき飛ばす | 47 / 200 | 50(6.12) |
1700
+ | アプリカティブ | 片方の理由を捨てる | 56 / 200 | 50(6.12) |
1701
+ | プロファンクタ | 片方だけ直す | 188 / 200 | 180(4.24) |
1702
+
1703
+ ### 8.5 テストの種類
1704
+
1705
+ | テストの種類 | 確かめること | 道具 |
1706
+ | --- | --- | --- |
1707
+ | 例によるテスト | 代表的な入力での振る舞い | `#[test]`、`assert_eq!` |
1708
+ | スタブを使ったテスト | 副作用の失敗や組み合わせでの振る舞い | トレイトを実装したスタブ |
1709
+ | プロパティベーステスト | どんな入力でも成り立つ性質 | `proptest`、または自作の乱数 |
1710
+ | 法則のテスト | 代数構造の実装が法則を守ること | 乱数と、壊したときの検出率 |
1711
+ | 結合テスト | 実際のデータベースとの接続 | PostgreSQL(Zettai 版は実体を見せるため Testcontainers を使わなかった) |
1712
+
1713
+ ---
1714
+
1715
+ ## 9. 静的解析とルール設定
1716
+
1717
+ 関数型の規約(`unwrap` を避ける、副作用を境界に寄せる、ワイルドカードで網羅を無効にしない)は、レビューだけに頼ると漏れる。Rust では、コンパイラと clippy が、多くを機械で守る。
1718
+
1719
+ ### 9.1 ツールの選び方
1720
+
1721
+ | ツール | 役割 | Rust への適用 |
1722
+ | --- | --- | --- |
1723
+ | rustc(コンパイラ) | 型、所有権、借用、`match` の網羅、`#[must_use]` | 常に有効。lint の水準は `Cargo.toml` の `[lints.rust]` で上げられる |
1724
+ | rustfmt | 整形 | 書式の統一 |
1725
+ | clippy | 慣用から外れた書き方、バグの兆候、規約の検査 | 中心になる静的解析。`[lints.clippy]` と `clippy.toml` で規約を表す |
1726
+ | カバレッジ | テストが見ていないコード | テストが通ったかではなく、見ていない範囲を知る |
1727
+
1728
+ Kotlin 編で使った detekt と ktlint は Kotlin 用のツールで、Rust のソースは対象にならない。PMD は 7.9.0 から Rust に対応しているが、対応しているのは重複コードの検出(CPD)だけで、PMD のルール(静的解析)は Rust に対して使えない([PMD の Rust 対応](https://pmd.github.io/pmd/pmd_languages_rust.html)で確認)。そのため、Kotlin 編のような PMD の XPath ルールは使えない。Rust の規約は、clippy の設定で表す。clippy の組み込みの lint で足りない規約を表したいときは、独自の lint を書く仕組み(dylint など)があるが、本ガイドでは扱わない。
1729
+
1730
+ ### 9.2 検査に何を入れるか
1731
+
1732
+ Zettai 版は、検査の段を入れるときに、それが何を守るかを数えた。
1733
+
1734
+ | 段 | 何を守るか | `test` と重複するか | 秒数 |
1735
+ | --- | --- | --- | --- |
1736
+ | `fmt` | 体裁 | しない(`test` は体裁を見ない) | 1 秒 |
1737
+ | `clippy` | 慣用から外れた書き方 | しない(コンパイルは通るが指摘される書き方がある) | 3 秒 |
1738
+ | `test` | 振る舞い。あわせて型・所有権も(コンパイルするため) | — | 3 秒 |
1739
+ | `cov` | テストが見ていないコード | しない(`test` は「通ったか」しか見ない) | 3 秒 |
1740
+
1741
+ 4 段とも守るものが違うので、4 段とも入れた。前の対象(なでしこ3 版)では、`lint` が `test` と完全に重複していたのに、後の工程まで気づかず、検査時間の半分を食っていた。入れる前に、何を守るかを書き出す。
1742
+
1743
+ ### 9.3 clippy が止めたものの扱い
1744
+
1745
+ clippy は、正しい指摘をすることが多い。ただし、法則のテストのように、clippy の指摘が「確かめたい性質そのもの」を消してしまうことがある。恒等関数を `map` するのは、ふだんは冗長だが、ファンクタの恒等則のテストでは、まさにそれを確かめたい。
1746
+
1747
+ ```rust
1748
+ /// **clippy が `map_identity` で止める。** 「恒等関数を `map` するな」は
1749
+ /// ふだんは正しいが、**ここではそれが確かめたい法則そのもの**。
1750
+ /// 消すとテストが消える。
1751
+ #[allow(clippy::map_identity)]
1752
+ ```
1753
+
1754
+ Zettai 版は、clippy に押された件数を 5 件数えた。これは「設計の改善」と「clippy の好み」に仕分けた。法則のテストを止めた指摘は、好みの側に仕分け、理由をコメントに書いて `allow` した。`allow` するときは、理由を残す。
1755
+
1756
+ ### 9.4 コンパイラと clippy が守る範囲
1757
+
1758
+ 守る範囲は、思っているより狭いところがある。Zettai 版の第 13 章は次のように整理した。
1759
+
1760
+ | 何を | 守ったか |
1761
+ | --- | --- |
1762
+ | 境界(ドメインが HTTP を呼ばない) | 守った(クレートを分ける) |
1763
+ | 遷移表の枝が揃っているか | 守った(E0004) |
1764
+ | 遷移表の行き先が正しいか | 守らない |
1765
+ | ワイルドカードで網羅を無効にすること | 守らない(組で受けると lint も効かない) |
1766
+ | 失敗を捨てること | 守った(`Result` は `#[must_use]`) |
1767
+ | CQRS でコマンド側とクエリ側を混ぜること | 守った(型が違う) |
1768
+ | クエリ側から状態を変えること | 守らない |
1769
+
1770
+ `match` の穴を 3 種類作って確かめた結果は次のとおりである。
1771
+
1772
+ | 穴の種類 | 例 | コンパイラ | clippy |
1773
+ | --- | --- | --- | --- |
1774
+ | マスが足りない | `(HasItems, AddItem)` を書き忘れる | 止まる | — |
1775
+ | ワイルドカードが隠す(単体の `enum`) | `match state { _ => .. }` | 通る | 止められる |
1776
+ | ワイルドカードが隠す(組・参照ごし) | `match (state, command) { _ => .. }` | 通る | 通る |
1777
+ | 行き先が間違っている | 無いリストに項目を足せてしまう | 通る | 通る |
1778
+
1779
+ コンパイラが「ワイルドカードを足せば通る」と案内するので、足すと網羅の検査がそこで止まる。ワイルドカードを禁じる `clippy::wildcard_enum_match_arm` は、単体の `enum` を直接 `match` したときだけ発火し、遷移表のように組で受けると発火しない。コンパイラがあっても、守る範囲が違うので、守られない範囲はテストで確かめる。
1780
+
1781
+ ### 9.5 ツールを入れる前に、守りたいことを決める
1782
+
1783
+ 既製品や検査を足すときは、まず実際に起きている問題を数え、足したときの副作用(依存の数、ビルド時間、記事のコードへの影響)を測る。Zettai 版は、この手順を 7 回の判断で繰り返し、入れたのは 1 回だけだった(10.1 節)。
1784
+
1785
+ | 状況 | 推奨 |
1786
+ | --- | --- |
1787
+ | 複数人の開発、長く保守するプロダクト | clippy の lint を `deny` にして CI で止める。`[workspace.lints]` で全クレートに揃える |
1788
+ | 1 人の教材・記事のコード | rustfmt と clippy は入れる(秒数が小さい)。独自の検査は、実際に起きた問題に絞る |
1789
+ | 層ごとに許す範囲が違う | クレートを分け、クレートごとに `clippy.toml` と `[lints]` を置く |
1790
+
1791
+ ### 9.6 関数型ルールを clippy の設定にする
1792
+
1793
+ 各章の規約を、clippy の設定で表す。`Cargo.toml` の `[lints]` で lint の水準を上げ、`clippy.toml` で、使ってはいけない関数や型を指定する。
1794
+
1795
+ > **検証状況**: このガイドのために書いた設定で、**clippy 0.1.91(Rust 1.91 系)で実行して確認した**。下の確認用コードに対して、17 件が期待どおりの行で報告された。ほかの版では、「ルールの確認」の手順で先に確かめること。
1796
+
1797
+ #### 章とルールの対応
1798
+
1799
+ | 章 | 規約 | 設定 |
1800
+ | --- | --- | --- |
1801
+ | 1 基礎 | 状態を持たせない。共有状態は境界のクレートだけが持つ | `clippy.toml` の `disallowed-types`: `std::sync::Mutex` |
1802
+ | 1 基礎 | 安全でないコードを書かない | `[lints.rust]` の `unsafe_code = "forbid"` |
1803
+ | 2 関数型スタイル | 添字の代わりに `get` とイテレータを使う | `indexing_slicing` |
1804
+ | 3 型で表すエラー | 失敗を panic にしない | `unwrap_used`、`expect_used`、`panic`、`todo` |
1805
+ | 3 型で表すエラー | `match` でワイルドカードを避ける | `wildcard_enum_match_arm`、`match_wildcard_for_single_variants` |
1806
+ | 3 型で表すエラー | 失敗を捨てない | `let_underscore_must_use`、`[lints.rust]` の `unused_must_use` |
1807
+ | 4 副作用の管理 | 標準出力を使わない | `print_stdout`、`print_stderr`、`dbg_macro` |
1808
+ | 4 副作用の管理 | 時計と環境は引数で受け取る | `disallowed-methods`: `SystemTime::now`、`Instant::now`、`env::var` |
1809
+ | 5 並行処理 | スレッドを塞がない、直接扱わない | `disallowed-methods`: `thread::sleep`、`thread::spawn` |
1810
+ | 6 代数構造の実用 | 法則を守っていること | lint では表せない。8.4 節の法則のテストで確かめる |
1811
+ | 7 アーキテクチャ | ドメインが外部に依存しない | クレートを分ける(lint ではなく、依存関係で守る) |
1812
+ | 8 テスト戦略 | テストでは `unwrap` などを許す | `clippy.toml` の `allow-unwrap-in-tests` など |
1813
+
1814
+ `disallowed_methods` と `disallowed_types` は、既定では警告になる。`deny` にして初めてビルドを止められる。
1815
+
1816
+ #### 設定
1817
+
1818
+ ```toml
1819
+ [package]
1820
+ name = "fp_lints"
1821
+ version = "0.1.0"
1822
+ edition = "2021"
1823
+
1824
+ [lints.rust]
1825
+ unsafe_code = "forbid"
1826
+ unused_must_use = "deny"
1827
+
1828
+ [lints.clippy]
1829
+ unwrap_used = "deny"
1830
+ expect_used = "deny"
1831
+ panic = "deny"
1832
+ todo = "deny"
1833
+ indexing_slicing = "deny"
1834
+ wildcard_enum_match_arm = "deny"
1835
+ match_wildcard_for_single_variants = "deny"
1836
+ print_stdout = "deny"
1837
+ print_stderr = "deny"
1838
+ dbg_macro = "deny"
1839
+ let_underscore_must_use = "deny"
1840
+ disallowed_methods = "deny"
1841
+ disallowed_types = "deny"
1842
+ ```
1843
+
1844
+ ```toml
1845
+ disallowed-methods = [
1846
+ { path = "std::time::SystemTime::now", reason = "時計は引数で受け取る" },
1847
+ { path = "std::time::Instant::now", reason = "時計は引数で受け取る" },
1848
+ { path = "std::thread::sleep", reason = "スレッドを塞ぐ" },
1849
+ { path = "std::thread::spawn", reason = "スレッドを直接扱わない" },
1850
+ { path = "std::panic::catch_unwind", reason = "失敗は Result で返す" },
1851
+ { path = "std::env::var", reason = "環境は引数で受け取る" },
1852
+ ]
1853
+ disallowed-types = [
1854
+ { path = "std::sync::Mutex", reason = "共有状態は境界のクレートだけが持つ" },
1855
+ ]
1856
+ ```
1857
+
1858
+ #### 確認用コード
1859
+
1860
+ 違反するコードと、しないコードを 1 つのファイルに並べ、期待どおりの行だけが報告されることを見る。
1861
+
1862
+ ```rust
1863
+ use std::sync::Mutex;
1864
+
1865
+ pub enum State {
1866
+ Empty,
1867
+ HasItems,
1868
+ Done,
1869
+ }
1870
+
1871
+ pub fn first(v: &[i32]) -> i32 {
1872
+ v[0]
1873
+ }
1874
+
1875
+ pub fn unwrap_it(o: Option<i32>) -> i32 {
1876
+ o.unwrap()
1877
+ }
1878
+
1879
+ pub fn expect_it(o: Option<i32>) -> i32 {
1880
+ o.expect("ある")
1881
+ }
1882
+
1883
+ pub fn boom() {
1884
+ panic!("boom");
1885
+ }
1886
+
1887
+ pub fn later() {
1888
+ todo!()
1889
+ }
1890
+
1891
+ pub fn say() {
1892
+ println!("x");
1893
+ dbg!(1);
1894
+ }
1895
+
1896
+ pub fn now() -> std::time::SystemTime {
1897
+ std::time::SystemTime::now()
1898
+ }
1899
+
1900
+ pub fn nap() {
1901
+ std::thread::sleep(std::time::Duration::from_millis(1));
1902
+ }
1903
+
1904
+ pub fn spawn() {
1905
+ std::thread::spawn(|| {}).join().ok();
1906
+ }
1907
+
1908
+ pub fn env() -> Result<String, std::env::VarError> {
1909
+ std::env::var("X")
1910
+ }
1911
+
1912
+ pub static LOCK: Mutex<i32> = Mutex::new(0);
1913
+
1914
+ pub fn wildcard(s: State) -> i32 {
1915
+ match s {
1916
+ State::Empty => 0,
1917
+ _ => 1,
1918
+ }
1919
+ }
1920
+
1921
+ pub fn wildcard_tuple(s: State, c: bool) -> i32 {
1922
+ match (s, c) {
1923
+ (State::Empty, true) => 0,
1924
+ _ => 1,
1925
+ }
1926
+ }
1927
+
1928
+ pub fn discard(r: Result<i32, String>) {
1929
+ let _ = r;
1930
+ }
1931
+
1932
+ pub fn raw(x: &i32) -> i32 {
1933
+ unsafe { *(x as *const i32) }
1934
+ }
1935
+
1936
+ pub fn pure_ok(v: &[i32]) -> Option<i32> {
1937
+ v.first().copied()
1938
+ }
1939
+
1940
+ pub fn exhaustive(s: State) -> i32 {
1941
+ match s {
1942
+ State::Empty => 0,
1943
+ State::HasItems => 1,
1944
+ State::Done => 2,
1945
+ }
1946
+ }
1947
+ ```
1948
+
1949
+ ```bash
1950
+ cargo clippy
1951
+ ```
1952
+
1953
+ 期待する結果は、次の 17 件である(`src/lib.rs` の行番号)。
1954
+
1955
+ | 行 | 報告される内容 | 対応する設定 |
1956
+ | --- | --- | --- |
1957
+ | 1 | `std::sync::Mutex` | `disallowed-types` |
1958
+ | 10 | 添字で panic しうる | `indexing_slicing` |
1959
+ | 14 | `unwrap()` | `unwrap_used` |
1960
+ | 18 | `expect()` | `expect_used` |
1961
+ | 22 | `panic!` | `panic` |
1962
+ | 26 | `todo!` | `todo` |
1963
+ | 30 | `println!` | `print_stdout` |
1964
+ | 31 | `dbg!` | `dbg_macro` |
1965
+ | 35 | `SystemTime::now` | `disallowed-methods` |
1966
+ | 39 | `thread::sleep` | `disallowed-methods` |
1967
+ | 43 | `thread::spawn` | `disallowed-methods` |
1968
+ | 47 | `env::var` | `disallowed-methods` |
1969
+ | 50(2 件) | `static` の型と `Mutex::new` | `disallowed-types` |
1970
+ | 55 | ワイルドカードの `match`(単体の `enum`) | `wildcard_enum_match_arm` |
1971
+ | 67 | `let _ = r;` で `Result` を捨てる | `let_underscore_must_use` |
1972
+ | 71 | `unsafe` ブロック | `unsafe_code` |
1973
+
1974
+ 次のコードは報告されない。組で受ける `match`(`wildcard_tuple`)、`first()` を使う `pure_ok`、網羅した `match`(`exhaustive`)である。組で受けるワイルドカードが通ることは、9.4 節の表のとおりで、lint では守れない範囲として残る。
1975
+
1976
+ `catch_unwind` を `disallowed-methods` に入れているが、確認用コードには含めていない(確認していない)。
1977
+
1978
+ #### テストでは許す
1979
+
1980
+ テストでは、`unwrap` や添字を使うほうが読みやすい。`clippy.toml` で、テストの中だけ許す。
1981
+
1982
+ ```toml
1983
+ allow-unwrap-in-tests = true
1984
+ allow-expect-in-tests = true
1985
+ allow-indexing-slicing-in-tests = true
1986
+ allow-panic-in-tests = true
1987
+ allow-print-in-tests = true
1988
+ ```
1989
+
1990
+ 確認したところ、`#[cfg(test)]` の中の `unwrap`、添字、`println!` は報告されず、同じファイルの本体の添字は報告された。`cargo clippy --all-targets` で、テストも検査の対象に含める。
1991
+
1992
+ #### 層ごとに許す範囲を変える
1993
+
1994
+ 層ごとに許す範囲が違うなら、クレートを分ける。ドメインのクレートには厳しい設定を置き、境界のクレート(HTTP、データベース)には置かない。`clippy.toml` は、クレートのディレクトリのものが使われる。
1995
+
1996
+ ```toml
1997
+ [workspace]
1998
+ members = ["domain", "adapter"]
1999
+ resolver = "2"
2000
+
2001
+ [workspace.lints.clippy]
2002
+ unwrap_used = "deny"
2003
+ disallowed_methods = "deny"
2004
+ ```
2005
+
2006
+ ```toml
2007
+ [package]
2008
+ name = "domain"
2009
+ version = "0.1.0"
2010
+ edition = "2021"
2011
+
2012
+ [lints]
2013
+ workspace = true
2014
+ ```
2015
+
2016
+ `domain` に `clippy.toml`(`disallowed-methods` に `SystemTime::now`)を置き、`adapter` には `[lints]` も `clippy.toml` も置かない構成で確認した。同じ `SystemTime::now()` と `unwrap()` を両方のクレートに書いたところ、`domain` の 2 件だけが報告され、`adapter` は報告されなかった。Kotlin 編のように除外パターンで層を外すのではなく、クレートの設定が層の境界になる。
2017
+
2018
+ #### 限界
2019
+
2020
+ - **意味は検査できない。** 遷移表の行き先が正しいか、法則が守られているかは、lint では表せない。テストで確かめる。
2021
+ - **関数の名前で判断する。** `disallowed-methods` は、関数の完全なパスで一致させる。ラッパー関数の中に隠した呼び出しは、そのラッパーの定義で報告される。
2022
+ - **ワイルドカードは組で通る。** `match (state, command) { _ => .. }` は、lint が止めない。レビューで見る。
2023
+ - **`catch_unwind` は未確認。** 設定には入れたが、動作は確認していない。
2024
+
2025
+ #### 導入の進め方
2026
+
2027
+ 1. まず `warn` で違反の件数を数える(9.5 節)。
2028
+ 2. 件数が多い lint は、新しいコードだけに適用する。
2029
+ 3. 例外を許すときは、`#[allow]` を乱用せず、理由をコメントに書く(9.3 節)。層ごとに許すなら、クレートを分ける。
2030
+ 4. lint を増やすのは、同じ指摘をレビューで繰り返したときに限る。
2031
+
2032
+ ---
2033
+
2034
+ ## 10. 判断の指針
2035
+
2036
+ ### 10.1 自前で作るか、既製品を使うか
2037
+
2038
+ Zettai 版は、既製品を 7 回判断し、入れたのは 1 回だった。
2039
+
2040
+ | 章 | 既製品 | 要求を満たすか | 判断 | 決め手 |
2041
+ | --- | --- | --- | --- | --- |
2042
+ | 3 | `cucumber` | 満たす | 入れず | 依存 +121・ビルド +92 秒 |
2043
+ | 5 | `proptest` | 満たす | 入れず | 依存 +38・26.77 秒 |
2044
+ | 7 | `thiserror` | 満たす | 入れず | 中身は `impl From` 1 つ |
2045
+ | 9 | `serde_json` | 満たさない | 入れず | 対にならない |
2046
+ | 11 | `tera` | 満たす | 入れた | 自作する理由が消えた |
2047
+ | 12 | `tracing` | 満たす | 入れず | 依存 +18。`check` の余裕が 4 秒弱 |
2048
+ | 12 | `serde` derive | 満たす | 入れず | 依存 0 が壊れる |
2049
+
2050
+ 入れなかった 6 回の理由は同じではない。数字が合わないが 4 回、要求を満たさないが 1 回、別の性質が壊れるが 1 回だった。第 11 章だけ結論が変わった。Kotlin 版が自作した理由(既製品は未適用のタグを失敗にできない)が、Rust では成り立たなかったからである。毎回調べることを手順にしていなければ、11 回目も自作していた。
2051
+
2052
+ これは「自作が好き」ではなく、教材の要求が特殊だからである。実務では、ライブラリを使ったほうが速く、保守される。
2053
+
2054
+ | | Kotlin | Rust |
2055
+ | --- | --- | --- |
2056
+ | 何を決めたか | 何を作るか | 与えられたものを使うか、作るか |
2057
+ | ADR の件数 | 12 件 | 14 件 |
2058
+
2059
+ 書くコードが減っても、判断は減らない。
2060
+
2061
+ ### 10.2 落とし穴
2062
+
2063
+ | 落とし穴 | 対処 |
2064
+ | --- | --- |
2065
+ | 一時値が借用中に破棄される(E0716) | 一度変数に受けてから使う。2 行に分かれて読みやすくなる |
2066
+ | `Vec<String>` と `&[&str]` を比べる(E0308) | 期待値を `&str` で並べ、比べる直前に `String` へ揃える |
2067
+ | `map` や `flat_map` のクロージャが借用を閉じ込められない | `move` で所有する。必要なら `clone` する |
2068
+ | 合成を `impl Fn` で表すと畳み込めない(E0308) | `Box<dyn Fn>` の値にして名前を付ける |
2069
+ | ハブの戻り値に `impl Fn` を 2 つ書けない | `Box<dyn Fn>` にして名前を付ける |
2070
+ | トランザクションを値として返せない(E0515・E0505) | 区間として貸す(`in_transaction`) |
2071
+ | `match` にワイルドカードを足して網羅を止める | `wildcard_enum_match_arm` を使う。ただし組で受ける `match` は止まらないので、レビューで見る |
2072
+ | `unwrap` で panic を持ち込む | `?` と `Result`。テストの中だけ許す |
2073
+ | `Mutex` を `lock().unwrap()` する | 毒された `Mutex` では panic する。境界のクレートだけが持ち、`expect` に理由を書く |
2074
+ | clippy が法則のテストを止める | `#[allow]` と理由のコメントで許す |
2075
+ | 同期で書けるのに `async` を足す | `async` は後戻りの効かない判断。規模が小さいなら同期で始める |
2076
+
2077
+ ### 10.3 型は何を守り、何を守らなかったか
2078
+
2079
+ 9.4 節の表のとおり、境界、遷移表の枝、失敗の無視、コマンドとクエリの型の混在は守られる。遷移表の行き先、ワイルドカード、クエリ側からの状態変更は守られない。コンパイラがあっても、守る範囲が違うので、守られない範囲はテストで確かめる。
2080
+
2081
+ ### 10.4 制約が設計を押す
2082
+
2083
+ Rust の所有権・借用・ライフタイムは、設計を 7 回押した。7 回とも、押された先のほうが読みやすい形だった。
2084
+
2085
+ | # | 押された箇所 | 押された先 |
2086
+ | --- | --- | --- |
2087
+ | 1 | 一時値が借用中に破棄された(E0716) | 一度変数に受ける。2 行に分かれて読みやすくなった |
2088
+ | 2 | 同じクレートだと相互参照できてしまう | クレートを分けた。境界検査が要らなくなった |
2089
+ | 3 | 合成を型で表すと畳み込めない(E0308) | 合成を値で持つ。モノイドの形がそのまま出た |
2090
+ | 4 | クロージャに借りた値を閉じ込められない | 借りずに所有する。イベントの形に合う |
2091
+ | 5 | ハブの戻り値に `impl Fn` を 2 つ書けない | `Box<dyn Fn>` にして名前を付けた。配線が 1 か所に寄った |
2092
+ | 6 | トランザクションを値として返せない(E0515・E0505) | 区間として貸す。`commit` の呼び忘れが起こりえなくなった |
2093
+ | 7 | 中身が空の文脈を作れない(E0478) | 文脈に動詞を持たせた。Kotlin 版の実行時検査が要らなくなった |
2094
+
2095
+ ### 10.5 導入の順序
2096
+
2097
+ 一度にすべてを取り入れる必要はない。効果が大きく、リスクの小さい順に進める。
2098
+
2099
+ 1. 束縛をデフォルトのイミュータブルのまま使い、`mut` は局所的な範囲に限る。
2100
+ 2. 副作用のある処理と純粋な計算を、別の関数に分ける。
2101
+ 3. panic の代わりに `Option` と `Result` を返す。`?` で合成する。
2102
+ 4. 副作用を境界に寄せ、トレイトや関数の引数として外から渡す。
2103
+ 5. rustfmt と clippy を CI に入れ、規約を `[lints]` と `clippy.toml` にする。
2104
+ 6. 状態の更新をイベントの列にする設計は、変更の履歴が価値を持つ領域に限って検討する。
2105
+ 7. `async` と tokio は、並行や I/O の待ちが実際に必要になってから入れる。
2106
+
2107
+ ---
2108
+
2109
+ ## 11. 参考資料
2110
+
2111
+ - Grokking Functional Programming Rust 版: 純粋関数から並行処理、実践的なアプリケーションまでを章立てで学ぶ記事(出典)
2112
+ - Zettai Rust 版(From Objects to Functions 演習): ToDo アプリを題材に、HTTP、ドメインモデリング、イベントソーシング、関数型エラーハンドリングまでを実装する連載(出典)
2113
+ - 上記 2 つのシリーズへのリンクは [記事](../article/index.md) にある。
2114
+ - [関数型プログラミングガイド(Kotlin 編)](./関数型プログラミングガイド_Kotlin編.md): 同じ構成で Kotlin と Arrow を扱ったガイド
2115
+ - [Clippy のドキュメント](https://doc.rust-lang.org/clippy/): lint の一覧と設定
2116
+ - [tokio](https://tokio.rs/)、[futures](https://docs.rs/futures/): 非同期と並行処理
2117
+ - 関連ガイド: [テスト戦略ガイド](./テスト戦略ガイド.md)、[コーディングとテストガイド](./コーディングとテストガイド.md)、[ドメインモデル設計ガイド](./ドメインモデル設計ガイド.md)