3 ポイント 投稿者 GN⁺ 2026-02-23 | 1件のコメント | WhatsAppで共有
  • 型システムを活用し、ランタイム検証の代わりにコンパイル時に不変条件を保証する Rust の設計方式を説明する
  • NonZeroF32NonEmptyVec のような 新しい型(newtype) を定義し、不正な状態(0、空ベクタなど)を表現不可能にする
  • OptionResult で失敗を返す代わりに、関数引数の制約を強めることでエラーを事前に防ぐ
  • String::from_utf8serde_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_jsonfrom_str::<Sample> は JSON を構造体へパースし、フィールドの存在と型の整合性をコンパイル時に保証する
    • foobar フィールドの存在、型の一致、配列長などの制約が型レベルで確認される

4. 型駆動設計の二つの原則

  • 不正な状態を表現不可能にする
    • NonZeroF32 は 0 を、NonEmptyVec は空の状態を表現できない
    • 単純な検証関数(is_nonzero)では依然として不正な状態を表現できるため、不完全である
  • 検証はできるだけ早い段階で行う
    • 「Shotgun Parsing」のように検証がコード全体に散らばると、セキュリティ脆弱性(CVE-2016-0752 など)につながりうる
    • パース段階で全ての制約を確認すれば、その後のロジックは安全に実行できる

5. Rust における型ベースの証明と応用

  • Curry-Howard 対応に従えば、型は論理命題、値はその証明と見なせる
    • typenum クレートを使えば、コンパイル時に数学的関係(3 + 4 = 8)を検証できる
  • 型システムを利用して、プログラムの 正しさをコンパイル時点で証明できる

6. 実務での適用アドバイス

  • 外部 API が単純な型(booli32)を要求していても、内部では 意味のある enum や newtype で表現すること
    • 例: LightBulbState { On, Off } を定義し、From<LightBulbState> for bool を実装する
  • verify()do_something_fallible() のような単純な検証関数があるなら、パースによる構造化された型変換を検討すること
  • 副作用のない関数なら、Result<Infallible, MyError> のように 意図的に不可能な状態を型で表現できる

7. 結論

  • Rust の 型システムを検証ツールとして活用すれば、コードの明確さと安定性が向上する
  • Vecsqlxbon など Rust エコシステムのさまざまなツールが、すでに型ベース設計を活用している
  • すべての問題を型だけで解決できるわけではないが、検証ロジックを型へ引き上げるアプローチは保守性と安全性を高める
  • Rust の強力な型システムを最大限活用し、コンパイラがエラーを検出してくれるコードを書くことを勧める

1件のコメント

 
GN⁺ 2026-02-23
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 は、実用性と理論的純粋性の衝突を示す例で、ベクタのように扱うには制約が多い
    • 「correct by construction」が究極の目標だ
      NonZeroU32 のような型は単純だが、本当の力はドメインロジック全体を型として設計し、コンパイラにゲートキーパーの役割を担わせることにある
      そうすることで、デバッグの負担がランタイムから設計時点へ移る
    • 「make invalid states impossible/unrepresentable」というキーワードでも関連資料を探せる
      例として "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 aFin n 型がその例だ

    • Rust にもマクロベースで依存型を模倣するライブラリがある
      例: anodized紹介動画
    • 配列長を stdin から読むならコンパイル時にはわからないので、この種の検証は静的情報がある場合に限られる
    • こうした機能がもっと一般化してほしいという思いがある
  • 1つの型に複数の関数を持たせるアプローチもある
    Clojure のように1つのマップですべてのデータを表現し、標準ライブラリ全体でそれを操作できるようにするやり方だ

    • Perlis の「1つのデータ構造に100個の関数」という言葉と、「Parse, Don’t Validate」の間には緊張関係がある
      重要な不変条件を型に載せることもできるし、単純な関数で表現することもできる
      動的型付け言語にも似た効果を生む設計習慣がある
    • これは純粋な代替案というよりトレードオフ
      外部入力は結局パースしなければならないので、完全に置き換えられるわけではない
    • 「stringly typed language」批判に似て聞こえるが、実際にはデータの形を段階的に精製していく過程だ
    • バランスが重要だ
      構造的型システムでは 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%減少した事例を見たことがある

    • Go でも試みられているが、ポインタの濫用代数的データ型の不在のため、やや冗長になる
  • 同じ哲学をUI デザインシステムにも適用している
    CSS を後から検査するのではなく、グリッド単位でしか配置できない型を定義して、13px のような任意のマージンをコンパイルエラーにする
    こうするとデザインの決定性が保たれる

    • どんなツーリングを使っているのか気になる、という質問があった
  • C# の records + pattern matching はこのアプローチに近い
    F# の discriminated unions はさらに強力で、Result<'T,'Error> によって不正な状態を表現不可能にできる
    C# も今後ネイティブ DU が導入されれば、はるかにすっきりするだろう