世界を動かす技術を、日本語で。

Rustにおけるパース、バリデーションを行わず、型駆動設計

概要

  • Rustの型システムを活用し、 バリデーションを型レベルで表現 する設計手法について解説
  • 「parse-dont-validate」パターン をRustで実践する方法を紹介
  • ゼロ除算や空配列 など、典型的な失敗例を型安全に防ぐ例を提示
  • 新しい型(newtype) を使うことで、実行時エラーをコンパイル時に防止
  • 現実世界での応用例 や、型安全の利点も解説

Rustで実践する「parse-dont-validate」パターン

  • Rustコミュニティでは -parse-dont-validate というタグがあり、バリデーション関数を避け、 型で不変条件(invariant)を表現 する手法が推奨
  • Haskellで説明されることが多いが、 Rustでの実践例 を初心者向けに解説

ゼロ除算の例

  • 通常の関数例:fn divide(a: i32, b: i32) -> i32 { a / b } bが0の場合、実行時パニック が発生
  • 浮動小数点の例:fn divide_floats(a: f32, b: f32) -> f32 { a / b } 0割でもエラーが出ず、inf(無限大)になる
  • assert!でバリデーションを追加しても、 実行時エラーは防げない

Rustの型システムによる解決

  • OptionやResult を返すことで「失敗」を返せる 例:fn divide_floats(a: f32, b: f32) -> Option<f32>
  • しかし、 関数の返り値を弱める(weaken)設計 となり、呼び出し側で毎回エラー処理が必要
  • 引数側を強化(strengthen) し、 ゼロでない値のみ受け付ける型 を作成 例:struct NonZeroF32(f32);
  • コンストラクタでゼロを弾く ことで、不変条件を型で保証

newtypeパターンの利点

  • バリデーションを一度だけ で済ませ、 呼び出し側も返り値のエラー処理不要
  • 例:
    • let Some(a) = NonZeroF32::new(5) else { handle_error(); return; }
    • let [root1, root2] = newtyped_roots(a, 4, 7);
  • 関数内部のバリデーション・重複チェックが不要 となり、 DRY原則 にも合致

配列の空チェックの例

  • 通常:Vecの空チェックを 関数ごとに毎回実施 し、呼び出し側でも再チェックが必要
  • NonEmptyVec<T> 型を導入し、 空でないことを型で保証
    • 例:struct NonEmptyVec<T>(T, Vec<T>);
    • fn get_cfg_dirs() -> Result<NonEmptyVec<PathBuf>, Box<dyn Error>>
  • 型変換時に一度だけ検証 すれば、以降は 空チェック不要

バリデーション関数 vs パース関数

  • is_nonzero(f32) -> boolのようなバリデーション関数では 型安全性が担保できない
  • to_nonzero(f32) -> Option<NonZeroF32>のような パース関数 で、 意味を持った型へ変換 することで、 コードの可読性と安全性向上

実例:標準ライブラリやサードパーティでの活用

  • String型 も、内部的にはVec<u8>のnewtype
    • String::from_utf8UTF-8チェックと型変換 を一度で実施
    • 以降は String型として安全に利用可能
  • serde_json では、from_str::<Value>JSONバリデーションと型変換
    • ただし、Value型だと 都度バリデーションが必要
    • 型定義(スキーマ)を使うことで、型安全なデータ利用 が可能

型駆動設計のマキシム(Maxims)

  • 不変条件は型で表現 し、 バリデーションは型変換時に一度だけ
  • 返り値を弱めるのではなく、引数を強める設計 を優先
  • 型安全性の向上実行時エラーの削減
    • リファクタに対する耐性も高まる

まとめ

  • Rustの型システム を活用し、「parse-dont-validate」パターンで 安全かつ堅牢なAPI設計
  • バリデーションの責任を呼び出し側に移譲 し、関数内部の重複チェックを排除
  • 型で意味を表現 することで、 実行時エラーをコンパイル時に防止
  • 現実的な応用例 も多く、 API設計やリファクタ時の安全性向上 に寄与

Hackerたちの意見

最近の関連情報: Parse, Don't Validate (2019) - https://news.ycombinator.com/item?id=46960392 - 2026年2月 (172件のコメント) こちらも: Parse, Don’t Validate – 一部Cの安全性ヒント - https://news.ycombinator.com/item?id=44507405 - 2025年7月 (73件のコメント) Parse, Don't Validate (2019) - https://news.ycombinator.com/item?id=41031585 - 2024年7月 (102件のコメント) Parse, don't validate (2019) - https://news.ycombinator.com/item?id=35053118 - 2023年3月 (219件のコメント) Parse, Don't Validate (2019) - https://news.ycombinator.com/item?id=27639890 - 2021年6月 (270件のコメント) Parsix: Parse Don't Validate - https://news.ycombinator.com/item?id=27166162 - 2021年5月 (107件のコメント) Parse, Don’t Validate - https://news.ycombinator.com/item?id=21476261 - 2019年11月 (230件のコメント) Parse, Don't Validate - https://news.ycombinator.com/item?id=21471753 - 2019年11月 (4件のコメント) (追記: これらのリンクは好奇心旺盛な読者のために載せてるだけで、批判の意図はないよ!こういうことを言うと、みんなが勘違いしちゃうことがあるからね)

他の言語では、依存型のようなものを使ってさらに進められるよ。例えば、get_elem_at_index(array, index)のようなものは、配列の範囲外のインデックスを持つことができないってことを、コンパイル時に静的にチェックできるんだ。しかも、配列の長さを事前に知らなくてもね。「Idrisでは、長さインデックス付きベクターは Vect n a(長さ n が型に含まれる)で、長さ n への有効なインデックスは Fin n('n より厳密に小さい自然数')だよ。」分割して無限やマイナス無限になる可能性のある割り算でも同じようなトリックが使えるし、型チェックを防ぐための微妙な意味合いもあるんだ。例えば、高次型や関数においてね。

それってどういうこと?例えば、配列の長さがstdinから読み取られる場合、コンパイル時にそれを知るのは不可能じゃない?多分、何らかの制限があるのかな?

依存型がもっと一般的だったらいいのにな :(

Rustにはマクロを使った依存型を実現できるライブラリもあるよ。例えば: https://youtube.com/watch?v=JtYyhXs4t6w これは https://docs.rs/anodized/latest/anodized/ に関連してる。

これ、最近のStroustrupの発表を思い出すな。C++で必要なところで整数変換を自動的に検証するためにコンセプトを使うってやつ。https://www.stroustrup.com/Concept-based-GP.pdf { Number ii = 0; Number cc = '0'; ii = 2; // OK ii = -2; // throws cc = i; // OK もし i が cc の範囲内なら cc = -17; // char が符号付きなら OK; そうでなければ throws cc = 1234; // char が8ビットなら throws }

代わりに一つの型を使って、その型で動作できる多くの関数を持つっていうのもあるよね。Clojureが基本的にマップをどこでも使っていて、標準ライブラリ全体がそれをいろんな方法で操作できるようにしてる感じ。多くの型を使うアプローチの主な問題は、似たような型がいくつもあって、互換性がないことだね。

なんでこれがフラグ立てられてるのか全然わからない。自分はこれが真実だと思ってるけど、純粋な利点ってよりはトレードオフだと思うし。それに、外部の、通常は信頼できないソースからの入力を常にパースする必要があるっていう点では、あまり関係ない気がする。

そうだね、「100の関数が1つのデータ構造を操作する方が、10の関数が10のデータ構造を操作するより良い」っていうPerlisの引用と、Parse, don't validateの間には少し緊張感があるよね。でも、私が考えているのは、重要な不変条件を型や関数(特にシンプルな関数)にエンコードすることで、プログラムをうまく設計することが可能だってこと。Clojureのような動的型付け言語では、「Parse, Don't Validate」と同じような効果を持つデザインプラクティスがあるって経験してる。結局は、どのスタイルを好むかのマインドセットの問題だよね。

2つ以上の選択肢があるよ。関数は複数の型で動作できるからね。

これって「ストリングリー型言語」を揶揄するような話だね。実際にはどう違うの?

それは代替案じゃないよ。もっとダイナミックな型から始めて、形に関係ないことをやって、より正確な型にパースして、追加の不変条件に依存することをやって、またダイナミックな型に戻る感じ。

バランスが大事だと思う。構造型システムでブランドを使って名義型を扱ったり、名義型システムで構造型をある程度やったりもできるけど、あんまり使いやすくはないよね。結局、両方のミックスが一番いいと思う。

この記事で使われているゼロ除算の例は、「パース、バリデートしない」の原則を示すにはあまり良い例じゃないよ。カプセル化に依存してるからね。「パース、バリデートしない」の原則は、信頼できないデータを構造的に正しいデータ型に変換する関数で最もよく表現されるんだ。元の「パース、バリデートしない」の記事の著者アレクシス・キングも、フォローアップとして「名前は型安全ではない」という記事を発表していて、そこでは「newtype」パターン(例えば、非ゼロ整数をラッパー型で隠すこと)が、構造的に正しいことよりも弱い保証を提供することを明確にしているよ。彼女の元の記事には、次のような注意書きも含まれているんだ。> 抽象データ型を使ってバリデーターを「パーサーのように見せる」。時には、不正な状態を本当に表現できないようにするのは、Haskellが提供するツールを考えると実際には難しいこともある。例えば、整数が特定の範囲にあることを保証することとかね。その場合は、スマートコンストラクタを使ってバリデーターから「偽の」パーサーを作る抽象newtypeを使うといいよ。だから、内部データを保護する抽象データ型は、型システム自体が不変条件をエンコードできない場合に「パーサーに似せようとする」バリデーターなんだ。記事の二つ目の例である非空ベクタは、1つの要素が存在しなければならないという不変条件を型システム内にエンコードしているので、より良い例だね。アレクシス・キングの記事の要点は、プログラムは関数が構造的に正しいデータ型を返すように構成されるべきだということなんだ。これは、パーサーがあまり構造化されていないデータをより構造化されたデータに変換するのに似ているよ。

newtypeベースの「パース、バリデートしない」も実際にはすごく役立つよ。大事なのは、裸の文字列を持っていると「どこにあったのか」がわからないこと。すでにバリデートされているかどうかの情報を持っていないからね。たとえnewtypeが構造的に完全な正しさを提供できなくても、カプセル化された値の有効性を納得しやすいのは、裸の値と比べてずっと簡単だよ。完全な「パース、バリデートしない」を実現するには、基本的に依存型システムが必要なんだ。もっと軽量な部分的解決策として、Rustはパターンによって制約された型、つまりパターン型のプロトタイプを作っているよ。例えば、範囲制限された整数型は i8 is 0..100 と書けたり、非空スライスは [T] is [_, ..] と表現できたりする。こういった機能は、構造的に正しいことを簡単にするのに役立つだろうね。非空リストを (T, Vec) として実装するのは、実用性と理論的純粋性の衝突の良い例だよ。最初の要素を二重に保存しないと要素のスライス(連続的なビュー)を提供できないから、普通のVecとは異なり、イミュータビリティと T: Clone が必要になるので、あまり役に立たないんだ。もっと制限されたインターフェースを持つ抽象リストとして考えればいいけどね。

「無効な状態を不可能にする/表現できないようにする」って検索すると、関連する実践についての情報がもっと見つかるよ。「ドメインモデリングをファンクショナルに」っていうのもいい例だしね。

同意するよ、「構築による正しさ」がここでの最終目標だよね。NonZeroU32のような型を使うのはシンプルでいい例だけど、本当の力は、コンパイラがゲートキーパーとして機能するようにドメインロジック全体を設計したときに発揮されるんだ。これで、ランタイムのデバッグからデザインタイムの思考にメンタルローディングがシフトする。

記事では加算の実装についてすぐに触れてるね。impl Add for NonZeroF32 { ... } impl Add for NonZeroF32 { ... } impl Add for f32 { ... } でも、どの型を返すんだろう?

F32になるんじゃない? 結果の「非ゼロ性」を強制する方法が思いつかないんだけど、オプショナルなResultを返すようにしないといけなくなるし、そうなると結局元に戻っちゃうよね…

Optionみたいな感じになると思う。-2.0 + 2.0はランタイムで制約を違反するからね。これでまた Option のハンドリング問題が戻ってくる。記事は NonZeroPositiveF32 を例の型にしてた方が良かったと思う。そうすれば加算が安全になるから。

問題の例は、関連するコード全体に複雑さを広げるよね。これはRustでよく見る、抽象化が多すぎて複雑さが伴うケースだと思う。私は単純に(デフォルトとして;状況によるけど)… 除算の前にバリデートして、適切に処理するかな。よく遭遇する類似の状況はインデックスのチェックで、例えばインデックスが範囲外かどうかを確認すること。似たようなアイデアで、チェックしてエラーを表示したり印刷したりして、その計算を失敗させるけどプログラムはクラッシュしないようにするんだ。通常はバグの兆候で、それを追跡できるよね。あるいは、頻繁にインデックスされる配列なら、その配列を所有する構造体に (RustのコアにおけるCanonical) get メソッドを使うといいよ。それはOptionを返すからね。私はこの記事のアプローチでも、バリデートでも、ランタイムクラッシュよりはマシだと思う! プログラミングにはたくさんのパターンがあるけど、こういう使い方はOSSのRustでよく見るけど、私には合わないな。今回のケースではひどくはないけど、やる価値はないと思う。この記事の哲学の鍵は、最後の方にあるんだ。> 私はもっと多くの型を作るのが大好きです。五百万の型をみんなにどうぞ。

ここでの try_roots の例は、実は著者の主張に対する 反例 なんだよね。彼らは「負の判別式」のケースを無視してる。これを考慮したらどうなるの? 彼らの「パース」アプローチを取ると、引数の a, b, c の型は b^2 - 4ac >= 0 という制約を何らかの形でエンコードしなきゃいけない。これはめちゃくちゃになると思う。Rustでこれをクリーンにやる方法が思いつかないよ。単純に Option を返して、関数内でバリデーションをする方が ずっと 理にかなってる。一般的に、バリデーションが問題を解決する最良の方法だと思う。著者が投稿でこだわってる唯一の反例は、特定の値がクリーンで静的に検証可能な方法で制約されている場合だけど、ほとんどの場合、バリデーションは複数の値の間の(複雑な)相互作用をチェックするために使われるから、「パース」は全然便利じゃない。

この考え方が、UIデザインシステムをコンパイラのように扱うようになった理由なんだ。視覚的な出力を後から検証するのではなく(CSSのリンティングや手動デザインレビューみたいに)、制約を最初にパースするんだよ。レイアウトコンポーネントが離散グリッドの倍数だけを受け入れるように厳密に型付けされているなら、任意の13pxのマージンはコンパイルエラーになって、主観的なデザインの議論にはならない。これで決定論が強制されるんだ。

興味あるな、ここでどんなツールを使うつもり?