設計のためのメンタルモデル
同じ機能でも実装は複数ある。ここでは、コードを書く前に何を見るかを揃える。モデルは現実の一部を取り出す道具なので、使える範囲と取り落とすものも考える。
M1 — 値は「誰かが持ち、必要な間だけ貸す」
所有権を、値を使う責任の所在として見る。借用は所有者の責任を移さず、一時的に読む・変更する権限を渡す。
Trailでは、ファイルから読んだStringを保存層が返し、解析したEntryはアプリが持つ。集計は &[Entry] を借りればよい。別タスクに独立して渡す仕事なら、所有する値が必要になることが多い。
問う: この参照の所有者はどこで消えるか。必要なのは共有か、所有権の移動か、独立した複製か。
試す: title() -> &str と title() -> String を比べ、呼び出し元の制約と割り当ての違いを見る。cloneを減らすだけを目的にせず、寿命の複雑さも比べる。
M2 — 境界を越える時に、値の意味を決める
CLIの文字列、JSON、SQLの行は外部表現。Entry はプロダクトの意味を持つ値。外部入力の形が正しいことと、ドメインの条件を満たすことは別の検査。
v0では Entry::new が0分と空タイトルを拒否する。後でJSONを読む時も、Deserializeできたことだけで検証済みにしない。入力DTOから検証済みEntryへ変換する。
問う: 無効な値を作れる入口が他にないか。publicフィールドやderiveで条件を迂回していないか。
試す: タブや改行をタイトルに入れ、ファイルの区切りとして解釈されないことを確認する。後の形式変更でも同じ「記録として有効」という条件を守る。
M3 — 中心は判断、外側は副作用
集計や入力検査を、ネットワーク・時計・DBの都合から離して考える。外側が情報を集め、中心が判断し、外側が結果を保存・表示する。
flowchart LR
CLI[CLI / HTTP入力] --> Convert[解析・検証]
Convert --> Domain[Entry / 集計 / 状態遷移]
Domain --> Store[ファイル / SQLite]
Domain --> View[表示 / JSON応答]問う: このテストにDBや実時間は必要か。必要な副作用だけを境界として置けるか。
試す: total_minutes をファイルなしでテストする。保存処理のテストは別に、実際のファイルで再起動を越える結果を確認する。全てをtraitで包む必要はなく、差し替える理由ができた時に切り出す。
M4 — 状態と、許される遷移を分ける
ジョブの文字列を自由に更新すると「完了なのに再実行中」が生まれる。状態はenum、変更可能な組み合わせは遷移として考える。
問う: どこからどこへ進めるか。状態の確認と更新が別処理なら、その間に何が変わりうるか。
試す: ex08 の状態表を作る。その後DBで「現在Runningで、期待したversionと一致する時だけ完了へ更新」を実装する。enumで表せても、複数ワーカーの競合は保存側でも防ぐ必要がある。
M5 — asyncは「待つ間、実行権を返す」
asyncを速さの魔法として扱わない。I/Oの完了を待つ間に、ランタイムが他のタスクを進められる仕組みとして考える。CPUを使う処理は、awaitを置くだけで軽くならない。
問う: 今待っているのはネットワーク、ディスク、ロック、それとも実際に計算中か。awaitの前後で何を保持しているか。
試す: 小さいタイマー2つ、重い計算2つをそれぞれ動かす。ロックを持ったままawaitする設計と、必要な値だけ取り出してからawaitする設計を比べる。
参照: Tokio tutorial。
M6 — 待ち行列も、使える資源を消費する
処理より受付が速ければ仕事が溜まる。タスク数を増やすだけでは、その差は消えない。待っている仕事がメモリ・時間・接続を使う。
問う: 実行中の上限と、待ち行列の上限は何か。満杯なら待つのか、拒否するのか。待つ時間にも上限があるか。
試す: HTML図解で容量2・ワーカー1を指定し、6件投入する。Rustでは bounded_queue の例を実行する。拒否する仕事と、受け付けた仕事を区別する。
平均値だけでよい安定状態では「滞留する仕事 ≈ 到着率 × 滞留時間」と考えると、待ち時間が伸びる影響を見積もれる。急な負荷や停止では、この近似だけで判断しない。
参照: Littleの待ち行列の関係。
M7 — 確定したことと、観測したことは違う
DBに保存できても、応答が利用者に届かないことがある。利用者のtimeoutは、「処理されなかった」という証拠にはならない。
問う: 成功を返す前に何を確定するか。応答を失って同じ要求が来た時、同じ結果を返せるか。
試す: 要求IDを同じにして2回送る。記録と要求IDを同じトランザクションで保存し、件数が増えないことを確認する。メモリのSetだけでは、再起動後にこの保証が消える。
M8 — 再試行には、重複と上限がある
一時的な失敗は再試行で改善することがある。一方で、既に成功した処理を重複させたり、障害時の負荷を増やしたりする。
問う: この処理はもう1回してよいか。どの失敗が一時的か。試行回数・総時間・待ち時間の上限は何か。
試す: ex09 の上限付き遅延を実装し、初回・最大attempt・base=0を試す。プロダクトではjitterも検討し、実時計へ依存しないテストで待ち方を確かめる。
M9 — エラーは、利用者が次にできる行動を伝える
失敗の種類を「入力を直す」「再試行する」「管理者が調べる」に分けて考える。ドメインの失敗と、ファイルやDBの失敗を1つの文字列へ早く潰しすぎない。
問う: 呼び出し元が分類する必要はあるか。内部のパスや入力全文を応答・ログへ出していないか。
試す: 0分、存在しない親ディレクトリ、壊れた保存ファイルを比べる。APIでは分類をステータスへ変換し、内部原因は必要な情報だけログへ残す。
M10 — 性能は、測定する対象の文脈に属する
「Rustだから速い」から始めず、「1万件を集計する時、どこに時間とメモリを使うか」から始める。改善する量と、悪化させてもよい量を先に決める。
問う: 比較条件は同じか。実行時間、p95、割り当て、バイナリサイズのどれを見ているか。結果は何回測っているか。
試す: 同じ固定fixtureでreleaseビルドを複数回測る。1箇所だけ変え、時間だけでなく正しさとメモリも比べる。小さい差なら測定のばらつきを先に疑う。
M11 — 抽象化は、変化を局所に閉じるために選ぶ
traitはファイル数を増やすためのものではない。同じ契約で保存先を変える、外部時計をテスト時計へ変える、といった変化を閉じる道具。
問う: 次の変更はどこへ広がるか。genericsの静的な選択とdynの実行時の選択、どちらが必要か。
試す: 保存先をファイルとSQLiteで1回切り替える。traitを導入した場合と、enumで分岐した場合の変更箇所・読みやすさを比較する。将来の可能性だけで全てを抽象化しない。
M12 — 解放・取消・終了も、正常な経路として設計する
RAIIはスコープ終了時の片付けを結び付けるが、保存された仕事の意味まで自動では戻さない。futureをdropした時、処理の途中でどの副作用が残るか考える。
問う: 中断する時に何を止め、何を待つか。途中の書き込みやRunningのジョブは誰が復旧するか。
試す: 停止要求中に、新規受付を閉じて、進行中の処理を期限まで待つ。強制終了ではlease期限後に回収し、重複した完了を防ぐテストをする。
unsafeを読む時の補助モデル
safeな外部契約を成立させるために、内部が何を証明する必要があるかを見る。ポインタの有効性・初期化・寿命・aliasing・thread間の利用条件を列挙する。テスト通過だけで証明済みにしない。
プロダクトにunsafeを足すことは必須課題ではない。必要性が測定で示されてから、隔離した実験で扱う。参照: Rustonomicon。
2024〜2026の mental-model 更新:edition と agent は review 境界を足す
2024 project goals は ergonomic async と 2024 Edition を roadmap に置いた。Rust 1.85.0 は2025年にこの edition を stable にし、明示的な unsafe の期待と temporary/lifetime に関係する規則変更を含む。これは M1、M5、unsafe model を強める。edition migration は明示的な境界であり、lifetime や concurrency design が sound だという証明ではない。Cargo.toml の edition、rust-version、toolchain を設計記録に残し、cargo fix は保守的に実行して、出力を semantic recommendation とせず挙動を review する。
現在の公式 blog には2026-10-01の Rust 1.99.0 と、9月の security/maintainer 投稿が載る。現在の version 番号は supply-chain や recovery の model を置き換えない。M7〜M9 では dependency 一つを pin し、branch 上で許可 version range だけを変え、失敗した update が durable な Trail data を変えないことを示す演習を加える。M10 では、Rust や AI に性能差を帰す前に、同じ fixture で一つの release/toolchain 変更を測る。
Google の Argon 発表は、大規模 Rust migration が audit、emulation、review、compiler output の調査、profile-guided experiment を使うと報告する。これを実践的な AI 境界へ変える。生成 code は M2 の外部 input、生成 patch は test と人の review を通った後にだけ M3 を越える。r/rust の job と AI-post thread には異なる個人意見があるため community experience と明記する。そこから採用傾向、agent の信頼性、安全性を推論しない。
MENTAL MODEL / 考える順序
発表から、自分の判断へ。
発表の主張と、論文・公式ドキュメントの条件を並べて読む。
出典
01自分のノート