
2026/02/22 4:40
**「Rustにおけるパース・非検証設計と型駆動開発」**
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
要約
この記事は、Rust の型システムを利用して、プリミティブ値を newtype でラップすることでコンパイル時にドメイン不変条件をエンコードできることを示しています。生成関数を失敗可能(例:
NonZeroF32::new が Option<NonZeroF32> を返す)にすることで、値がゼロでないことを保証します。その後、関数は生のプリミティブではなくこれら newtype を受け取るようになり、検証を関数内部から呼び出し前へ移動させ、冗長なランタイムチェックを排除します。
同様のパターンが
NonEmptyVec<T> にも適用されます。これはベクタが空でないことを保証し、first() のような安全メソッドを追加のガードなしに提供します。著者は、これを Option や Result を返す通常の失敗可能関数と対比し、より強力な型保証が API 契約をどれだけ簡素化するかを強調しています。
実際の例として、Rust 自体の
String は検証済み Vec<u8> の newtype であり、Serde は JSON をデシリアライズしてコンパイル時にスキーマ制約を課す具体的な構造体へ変換できます。また、記事は「ショットガン解析」や Curry–Howard 対応といった関数型プログラミングの概念も参照し、タイプによる早期かつ包括的な検証の動機付けを行っています。
実務上のアドバイスとして、より意味のある型(例:単なるブールフラグではなく
LightBulbState のような列挙体)が正確性を向上させる場合はプリミティブを避けるべきです。newtyping はコードを冗長にすることがありますが、一般的にはより明確で安全な API を提供し、ランタイムエラーを減らします。主な制限点は、Rust が現在 newtype 用のエルゴノミック委譲(delegation)をサポートしていないことであり、将来の RFC やライブラリがこの障壁を低くする可能性があります。
この要約はキーポイントリストからすべての主要点を捉えつつ、メッセージを明確に保ち、余計な推論を排除しています。
本文
読了時間: 約17 分
目次
1.1 ゼロ除算
1.2 実際の例
1.3 型駆動設計の格言
1.4 何ができるか?
1.5 結論
写真は Tingley Injury Law Firm のものです。
Rust プログラミングコミュニティサーバーには
-parse-dont-validate というタグがあります。このタグは「バリデーション関数を避け、型レベルで不変性をエンコードする」記事へリンクしています。私は初心者・中級者に対し、API設計で苦労している方にこのタグを勧めることが多いです。
問題点は、その概念説明が Haskell で書かれていることです。
Haskell は関数型パラダイムの代表格ですが、関数型プログラミングに慣れていない初心者には敷居が高く感じられる場合があります。そこで今回は Rust の観点からこのパターンを紹介するブログ記事を書こうと思いました。さっそく始めましょう!
ゼロ除算
最も基本的な例として、整数を別の数で割る関数を考えます。
fn divide(a: i32, b: i32) -> i32 { a / b }
b が 0 のときはパニックします。
fn main() { let a = 5; let b = 0; dbg!(divide(a, b)); }
実行すると次のようなエラーが出ます:
thread 'main' panicked at src/main.rs:2:5: attempt to divide by zero
ランタイムで失敗を知らせることは悪くありません。しかし、もっと強固な保証を求める場合はどうでしょう? 例えば浮動小数点の場合です。
fn divide_floats(a: f32, b: f32) -> f32 { a / b }
b = 0.0 のとき inf(無限大)が返ってくるだけで、エラーにはならないのが問題です。
典型的な整数除算と同じ挙動にしたいのであれば、以下のように
assert! を入れます。
fn divide_floats(a: f32, b: f32) -> f32 { assert_ne!(b, 0.0, "Division by zero is not allowed."); a / b }
これで
b がゼロならランタイムでパニックしますが、まだ実行時失敗です。
Rust の型システムを使う
Rust では「失敗する可能性がある」関数に対しては
Option や Result を返す fallible パターンがよく用いられます。
fn divide_floats(a: f32, b: f32) -> Option<f32> { if b == 0.0 { None } else { Some(a / b) } }
これにより、関数が失敗する可能性を呼び出し側に明示し、必ずハンドリングさせます。
しかし、実行前に不変性をエンコードしておくこともできます。新しい型(newtype)を使います。
mod nonzero { pub struct NonZeroF32(f32); impl NonZeroF32 { fn new(n: f32) -> Option<Self> { if n == 0.0 { None } else { Some(Self(n)) } } } // 必要に応じてトレイト実装を追加 }
これで関数のシグネチャは次のようになります。
fn divide_floats(a: f32, b: nonzero::NonZeroF32) -> f32 { a / b.0 }
呼び出し側は
NonZeroF32 を渡さなければならず、内部でゼロ除算が起きることは保証できません。
例:二次方程式の解
以下は簡易実装です。
fn roots(a: f32, b: f32, c: f32) -> [f32; 2] { let discriminant = b * b - 4.0 * a * c; [ (-b + discriminant.sqrt()) / (2.0 * a), (-b - discriminant.sqrt()) / (2.0 * a), ] }
a == 0 や判別式が負の場合にパニックします。
対処法は二通りあります:
fn try_roots(a: f32, b: f32, c: f32) -> Option<[f32; 2]> { if a == 0.0 { return None } // … }
あるいは
fn newtyped_roots( a: nonzero::NonZeroF32, b: f32, c: f32, ) -> [f32; 2] { // 本体は変更なし、ただし `a` はゼロでないことが保証される }
newtype アプローチなら「ゼロチェック」を二度行う必要がなくなります。
例:設定ディレクトリ
fn get_cfg_dirs() -> Result<Vec<PathBuf>, Box<dyn Error>> { let cfg_dirs_string = std::env::var("CONFIG_DIRS")?; let cfg_dirs_list: Vec<_> = cfg_dirs_string.split(',').map(PathBuf::from).collect(); if cfg_dirs_list.is_empty() { return Err("CONFIG_DIRS cannot be empty".into()); } Ok(cfg_dirs_list) }
main でベクタが空でないか再度確認します。
fn main() -> Result<(), Box<dyn Error>> { let cfg_dirs = get_cfg_dirs()?; match cfg_dirs.first() { Some(cache_dir) => init_cache(cache_dir), None => unreachable!("already checked"), } }
代わりに
NonEmptyVec という newtype を定義できます。
struct NonEmptyVec<T>(Vec<T>); impl<T> NonEmptyVec<T> { fn first(&self) -> &T { &self.0[0] } // 空でないので安全 } fn get_cfg_dirs() -> Result<NonEmptyVec<PathBuf>, Box<dyn Error>> { let cfg_dirs_string = std::env::var("CONFIG_DIRS")?; let vec: Vec<_> = cfg_dirs_string.split(',').map(PathBuf::from).collect(); NonEmptyVec::try_from(vec).ok_or_else(|| "empty".into()) }
こうすれば
main は簡潔になります。
let cfg_dirs = get_cfg_dirs()?; init_cache(cfg_dirs.first());
冗長なチェックは不要です。
newtype が役立つ理由
- 不正状態を表現不能にする。
のようにゼロを取らない型なら、コンストラクタが失敗(NonZeroF32
)します。None - 不変性を早期に検証できる。
パースや構築時にデータを一度だけチェックし、その後はビジネスロジックで再確認する必要がなくなります。
実際のユースケース
- String – 内部は
で、Vec<u8>
によって UTF‑8 の検証が行われます。String::from_utf8 - Serde JSON デシリアライズ – 具体的な型(例:
) にパースすることで、再度Sample
を呼び出す必要がなくなり、フィールドの存在と型安全性をコンパイル時に保証します。unwrap!
型駆動設計の格言
- 不正状態は表現不能にする。
newtype で「有効値だけ」を構築できるようにします。 - 不変性は可能な限り早く証明する。
パース・構築を一度行い、その型をプログラム全体で再利用します。
これらの原則は関数型プログラミングから来ていますが、Rust の型システムに自然にマッピングできます。
実践的アドバイス
- 失敗し得るだけで必ず
を返す必要はありません。Result
失敗を型で表せるならそのほうが好ましいです。 - 第三者 API が原語(
,bool
等)を受け取る場合、可読性向上のために newtype や enum でラップすることを検討してください。i32
(例:
)LightBulbState { Off, On }
結論
newtype を使うことでドメイン固有の不変性を型システムに直接エンコードできます。これによりランタイムチェックが減り、コードは安全で明確になります。すべての問題を解決できるわけではありませんが、Rust の強力な型機能を活用することで、よりクリーンで堅牢なプログラムを書くことが可能です。
この考えに触発された Alex King に感謝します。