- 型システムを活用し、ランタイム検証の代わりにコンパイル時に不変条件を保証する Rust の設計方式を説明する
NonZeroF32、NonEmptyVec のような 新しい型(newtype) を定義し、不正な状態(0、空ベクタなど)を表現不可能にする
Option や Result で失敗を返す代わりに、関数引数の制約を強めることでエラーを事前に防ぐ
String::from_utf8 や serde_json::from_str のように、パースによって意味のある型へ変換する例を示す
- 不正な状態を表現不可能にし、検証をできるだけ前倒しする設計原則が、コードの安定性と可読性を高める
1. ランタイム検証の代わりに型で制約を表現
divide(a, b) 関数では、0 で割るとランタイムパニックが発生する
Option を返して失敗を表現することもできるが、これは返り値の型を弱める形になる
NonZeroF32 型を定義し、0 でない値だけを生成可能にする
- コンストラクタは
fn new(n: f32) -> Option<NonZeroF32> の形で、失敗時は None を返す
divide_floats(a: f32, b: NonZeroF32) と定義すれば、ランタイム検証は不要になる
- 検証の責務を 関数内部から呼び出し側へ移すことで、エラーを事前に取り除く
2. 重複検証の排除とコードの単純化
roots(a, b, c) 関数で a == 0 の検証を Option で扱うと、呼び出し側と関数の両方で重複検証が発生する
NonZeroF32 を使えば、検証は一度だけ行われ、その後のロジックは単純化される
- 同じ原理で
NonEmptyVec<T> を定義すれば、空ベクタを許可しない
get_cfg_dirs() が NonEmptyVec<PathBuf> を返せば、その後 main() で追加の検証は不要になる
3. 実例: String と serde_json
String は内部的には Vec<u8> の 新しい型(newtype) であり、String::from_utf8 が妥当性検査を行う
- 以後は UTF-8 が保証された文字列として安全に使える
serde_json の from_str::<Sample> は JSON を構造体へパースし、フィールドの存在と型の整合性をコンパイル時に保証する
foo、bar フィールドの存在、型の一致、配列長などの制約が型レベルで確認される
4. 型駆動設計の二つの原則
- 不正な状態を表現不可能にする
NonZeroF32 は 0 を、NonEmptyVec は空の状態を表現できない
- 単純な検証関数(
is_nonzero)では依然として不正な状態を表現できるため、不完全である
- 検証はできるだけ早い段階で行う
- 「Shotgun Parsing」のように検証がコード全体に散らばると、セキュリティ脆弱性(CVE-2016-0752 など)につながりうる
- パース段階で全ての制約を確認すれば、その後のロジックは安全に実行できる
5. Rust における型ベースの証明と応用
- Curry-Howard 対応に従えば、型は論理命題、値はその証明と見なせる
typenum クレートを使えば、コンパイル時に数学的関係(3 + 4 = 8)を検証できる
- 型システムを利用して、プログラムの 正しさをコンパイル時点で証明できる
6. 実務での適用アドバイス
- 外部 API が単純な型(
bool、i32)を要求していても、内部では 意味のある enum や newtype で表現すること
- 例:
LightBulbState { On, Off } を定義し、From<LightBulbState> for bool を実装する
verify() や do_something_fallible() のような単純な検証関数があるなら、パースによる構造化された型変換を検討すること
- 副作用のない関数なら、
Result<Infallible, MyError> のように 意図的に不可能な状態を型で表現できる
7. 結論
- Rust の 型システムを検証ツールとして活用すれば、コードの明確さと安定性が向上する
Vec、sqlx、bon など Rust エコシステムのさまざまなツールが、すでに型ベース設計を活用している
- すべての問題を型だけで解決できるわけではないが、検証ロジックを型へ引き上げるアプローチは保守性と安全性を高める
- Rust の強力な型システムを最大限活用し、コンパイラがエラーを検出してくれるコードを書くことを勧める
1件のコメント
Hacker News の意見
この記事で使われているゼロ除算の例は、「Parse, Don’t Validate」の原則を説明するには適切ではない
この原則の核心は、信頼できないデータを構造的に正しい型へ変換する関数にある
Alexis King の "Names are not type safety" でも、
newtypeパターンは完全な「correct by construction」を保証しないと述べられている型システムが不変条件を直接表現できないときは、**スマートコンストラクタ(smart constructor)**でパーサを模倣する抽象型を使うのが現実的なアプローチだ
2つ目の例である non-empty vec ははるかに良い例で、型システムの中で「常に1つ以上の要素が存在する」ことを保証する
newtypeベースの「parse, don’t validate」も実際には非常に有用だ文字列がどこから来たのかわからないとき、カプセル化された値は信頼性を大きく高める
完全な correctness-by-construction には依存型システムが必要だが、Rust の pattern types のような軽量な代替手段もある
たとえば
i8 is 0..100のように範囲を制限したり、[T] is [_, ..]で空でないスライスを表現できるただし
(T, Vec<T>)形式の non-empty list は、実用性と理論的純粋性の衝突を示す例で、ベクタのように扱うには制約が多いNonZeroU32のような型は単純だが、本当の力はドメインロジック全体を型として設計し、コンパイラにゲートキーパーの役割を担わせることにあるそうすることで、デバッグの負担がランタイムから設計時点へ移る
例として "Domain Modeling Made Functional" と 関連動画 は参考になる
このレベルでラップしようとするより、オーバーフローのような算術関数の挙動を包んでみたほうが違いをより明確に見られる
最近の関連議論へのリンクを整理した
Parse, Don't Validate (2019)(2026年2月、コメント172件)
Parse, Don’t Validate – Some C Safety Tips(2025年7月、コメント73件)
Parse, Don't Validate (2019)(2024年7月、コメント102件)など
単なる参考用として共有したものだ
Parsing over validation のアプローチには、現実のすべてのケースを把握できないとき限界がある
ファイルフォーマットのようにできるだけ早く失敗させるのは良いが、ビジネスロジックや状態遷移モデリングに適用するときは注意が必要だ
現実の要件が変わるとシステムがそれを受け入れられず、結局ユーザーが迂回することになる
他の言語では**依存型(dependent typing)**によってさらに先へ進める
たとえば
get_elem_at_index(array, index)では、配列長を事前に知らなくてもインデックス範囲をコンパイル時に保証できるIdris の
Vect n aとFin n型がその例だ例: anodized(紹介動画)
1つの型に複数の関数を持たせるアプローチもある
Clojure のように1つのマップですべてのデータを表現し、標準ライブラリ全体でそれを操作できるようにするやり方だ
重要な不変条件を型に載せることもできるし、単純な関数で表現することもできる
動的型付け言語にも似た効果を生む設計習慣がある
外部入力は結局パースしなければならないので、完全に置き換えられるわけではない
構造的型システムでは branding で名目的型を模倣することもでき、逆も可能だが、人間工学的ではない
結局のところ、両方を適切に混ぜるのが現実的だ
この議論は C++ の concepts 機能を思い起こさせる
Bjarne Stroustrup の Concept-based Generic Programming では、整数変換を自動で検証する例が示されている
Number<unsigned int>やNumber<char>型が範囲外なら例外を投げるという形だ記事の
try_rootsの例は実際には反例だb^2 - 4ac >= 0という制約を型で表現しようとすると、Rust では非常に複雑になるこういう場合は、単に
Optionを返して関数内部で検証するほうが合理的だほとんどの検証は複数の値の相互作用を扱うため、「パース」で解決するには不便だ
fn(abc: ValidABC)のような形でまとめるしかないこのパターンはAPI 設計にもよく合う
JSON リクエストを検証するより、最初から型保証された構造体にパースすれば、その後のロジックで重複検証は不要になる
Rust の serde + custom deserializer の組み合わせで簡単に実装できる
実際にこの方法でエラー処理コードが60%減少した事例を見たことがある
同じ哲学をUI デザインシステムにも適用している
CSS を後から検査するのではなく、グリッド単位でしか配置できない型を定義して、13px のような任意のマージンをコンパイルエラーにする
こうするとデザインの決定性が保たれる
C# の records + pattern matching はこのアプローチに近い
F# の discriminated unions はさらに強力で、
Result<'T,'Error>によって不正な状態を表現不可能にできるC# も今後ネイティブ DU が導入されれば、はるかにすっきりするだろう