概要
- 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>のnewtypeString::from_utf8で UTF-8チェックと型変換 を一度で実施- 以降は String型として安全に利用可能
- serde_json では、
from_str::<Value>で JSONバリデーションと型変換- ただし、Value型だと 都度バリデーションが必要
- 型定義(スキーマ)を使うことで、型安全なデータ利用 が可能
型駆動設計のマキシム(Maxims)
- 不変条件は型で表現 し、 バリデーションは型変換時に一度だけ
- 返り値を弱めるのではなく、引数を強める設計 を優先
- 型安全性の向上 と 実行時エラーの削減
- リファクタに対する耐性も高まる
まとめ
- Rustの型システム を活用し、「parse-dont-validate」パターンで 安全かつ堅牢なAPI設計
- バリデーションの責任を呼び出し側に移譲 し、関数内部の重複チェックを排除
- 型で意味を表現 することで、 実行時エラーをコンパイル時に防止
- 現実的な応用例 も多く、 API設計やリファクタ時の安全性向上 に寄与