HeadlinesBriefing favicon HeadlinesBriefing.com

Rustで解析する、検証しない:非空ベクター

Hacker News •
×

多くのプログラマーと同様に、私はアレクシス・キングの「解析する、検証しない」記事が興味深いと思います。なぜなら、それは私たちが過去に観察し、使用してきたが明示的に名前を付けていなかった慣用句に名前を与えているからです。この記事は、「解析する、検証しない」パターンをRustプログラミング言語に適用したレビューです(元の記事はHaskellを使用しています)。私は特に、Rustの標準ライブラリや他の有名なプロジェクトでこのパターンの教育的な例を見つけることに興味がありました。元の記事を繰り返しませんが(まず読んでください!)、その核心は次のとおりです。古典的なVecを考えてみましょう。その最初のメソッドはOption<&T>を返します。なぜでしょうか?なぜなら、ベクターには要素が保証されていないからです。空のベクターに対してfirstを呼び出した場合、どうすればよいでしょうか?この場合、Optionを返すことはRustにおける慣用的な方法です[1]。これは、Optionを返す関数の結果を受け入れ、次のステップを決定するための便利な構文糖を提供します。では、問題はどこにあるのでしょうか?環境変数から設定パスを読み取る関数があり、リストが空でないことを保証する制約があると仮定しましょう:use anyhow::{Result, ensure}; fn get_configuration_directories() -> Result<Vec<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories: Vec<Path Buf> = value.split(',').map(str::trim).map(Path Buf::from).collect(); ensure!(!directories.is_empty(), "empty CONFIG_DIRS"); Ok(directories) } ここまで、順調です。次に、この関数の典型的な使用例を見てみましょう:fn main() -> Result<()> { let config_dirs = get_configuration_directories()?; match config_dirs.first() { Some(cache_dir) => initialize_cache(cache_dir), None => unreachable!("already checked that CONFIG_DIRS is non-empty"); } Ok(()) } get_configuration_directoriesが成功した結果(Result)を返すと、ベクターが空でないことが保証されます。しかし、このベクターの最初の要素を取得するには、firstメソッドを使用する必要があり、これはOption<&T>を返します。したがって、私たちは再び、空の場合(つまり、OptionがNoneの場合)を処理することを余儀なくされます。元の記事では、これがコードの明確性、パフォーマンスへの潜在的な影響、およびget_configuration_directoriesの制約が変更された場合のタイムボムとして多くの問題を引き起こすと述べています。根本的な問題は、Vecが本質的に空である可能性のある型であるということです。私たちは、すべての関連コードに「このベクターは空でない、小さな約束!」というコメントを書くことができますが、それは形式的に何にも検証されません。 「非空」ベクターの型 解決策は、タイプシステムを利用して新しく確立された制約を強制することです。私たちは「空でないベクター」用の別々の型を使用できます。実際、このような型はすでにいくつかのRustクレートに存在します。例えば、nonempty: pub struct Non Empty<T> { pub head: T, pub tail: Vec<T>, } この型には「要素がない」ことを許容するコンストラクタはありません。そのnewは1つの要素を取り、firstメソッドはOptionなしで&Tを返します:pub const fn new(e: T) -> Self { Self::singleton(e) } pub const fn singleton(head: T) -> Self { Non Empty { head, tail: Vec::new() } } pub const fn first(&self) -> &T { &self.head } このクレートの残りの部分は、Non Emptyを通常のVecとできるだけ似たように振る舞わせるように努めています。多くの有用なトレイトを実装し、また、変換関数も提供します:pub fn from_vec(mut vec: Vec<T>) -> Option<Non Empty<T>> { if vec.is_empty() { None } else { let head = vec.remove(0); Some(Non Empty { head, tail: vec }) } } 私たちのget_configuration_directories関数が通常のVecではなくNon Emptyを返す場合、どうなるか見てみましょう:fn get_configuration_directories() -> Result<Non Empty<Path Buf>> { let value = env::var("CONFIG_DIRS").context("could not read CONFIG_DIRS")?; let directories = value.split(',').map(str::trim).map(Path Buf::from).collect(); let Some(directories) = Non Empty::from_vec(directories) else { bail!("CONFIG_DIRS cannot be empty"); }; Ok(directories) } ここでNon Empty::from_vecの使用に注目してください。これは制約が確立される場所です。さて...

FAQ質問:Rustで非空ベクタ型を使用する主な利点は何ですか?

FAQ回答:主な利点は、型レベルで非空の制約を強制し、最初の要素にアクセスする際のランタイムチェックとOptionの処理の必要性を排除し、コードの明確性と安全性を向上させることです。