型システムで「空でない」を証明する——Rustに見るParse, don't validateの実践
Rusty thoughts on "Parse, don't validate"
Alexis Kingの「Parse, don't validate」をRustで実践する方法を、標準ライブラリやrust-analyzerの具体例で解説。空でないVecをNonEmpty型で表現したり、絶対パスをAbsPathBufで型に刻んだり、NonZeroでゼロを排除するなど、検証を型に昇華させる設計が、コードの明確さと安全性をどう高めるかを示す。
検証ではなくパース——データを別の形式に変換することで、不変条件を型システムに強制させる。
HNでの議論
37- articulatepang
私はもう少し一般的なルールの方が好みです。それは「不正な状態を表現不可能にする(Make Illegal States Unrepresentable)」です。「Parse, don't validate」というルールは、MISUの特殊ケースにすぎません。
違いは何か? MISUは、パースのような変換が起きていない場合でも適用されます。例えば、ネットワーク接続の現在の状態を表す変数があり、それがDisconnectedか、あるIPアドレスにConnectedであるとしましょう(これは単純化しすぎですが)。
すると、一つの方法はこうです
struct {
connected: bool,
peer_ip: int32
}
問題は、これが不正/無意味な状態を表現できてしまうことです。つまり、切断されているのに、古いpeer_ipのジャンクがまだ残っているのです。さらに悪いことに、こう書くかもしれません
struct {
connected: bool,
peer_ip: Option<int32>
}
これでconnected = trueなのにpeer_ip = Noneという状態が可能になります。
解決策は直和型を使うことです:
type connection =
Disconnected
| Connected of int32
(でっち上げの構文ですみません。Rustに詳しい人なら明確だと思います。)
「不正な状態を表現不可能にする」は、プログラム全体に、モジュールや関数間のあらゆるインターフェースに適用され、入力のパースも含みますがそれに限りません。
- Fluorescence
その型が良いアドバイスだとは思えません:
pub struct NonEmpty<T> {
pub head: T,
pub tail: Vec<T>,
}
スライスやイテレーションの使い勝手をサポートするには手動でトレイトを実装する必要があり、Vecとして所有権を渡す必要がある場合にはコストのかかる再割り当てが発生します。
私ならこう期待します:
pub struct NonEmpty<T> {
v: Vec<T>,
}
コンストラクタが不変条件を強制し、その後、[T]に対してDerefとDerefMutを実装して通常のlen/is_empty/インデックス/イテレーションを手に入れ、他の関数に&[T]として渡し、値を変更できる(不変条件を壊すことはない)。
不変条件を保ちながら長さを変更するには、好みで選べばよい。例えば:
- unwrap/mutate/rewrap用に.into_vec()を追加する
- 好きな不変条件を保つミューテータを追加する
- Supermancho
「parse don't validate」は言い換えられる:
「パーサからの型検証を期待せよ」
これは型について、問題をただ動かしているだけだ。値の検証はずっと単純な問題で、アプリケーション固有の別のチェックとして行うものだ。
- jelder
これは素晴らしい。Alexis Kingは、「Parse, Don't Validate」がこんなに人気になるとは知っていたら、Haskellよりもっと広く使われている言語で書いただろうと実際に述べていた。
- jph
Rustの型の強みについての良い記事だ。これが好きなら、自分でパース機能をどう作るかに興味があるかもしれない。私はRustのクレートWinnowとNom、そしてRustのトレイトFromとIntoが好きだ。