注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
他のほとんどのプログラミング言語と同様、Rustはプログラマに、ある決まった作法でエラーを扱うことを促します。 一般的にエラーハンドリングは、例外、もしくは、戻り値を使ったものの、大きく2つに分類されます。 Rustでは戻り値を使います。
このセクションでは、Rustでのエラーハンドリングに関わる包括的な扱い方を提示しようと思います。 単にそれだけではなく、エラーハンドリングのやり方を、ひとつひとつ、順番に積み上げていきます。 こうすることで、全体がどう組み合わさっているのかの理解が進み、より実用的な知識が身につくでしょう。
もし素朴なやり方を用いたなら、Rustにおけるエラーハンドリングは、冗長で面倒なものになり得ます。 このセクションでは、エラーを処理する上でどのような課題があるかを吟味し、標準ライブラリを使うと、それがいかにシンプルでエルゴノミック(人間にとって扱いやすいもの)に変わるのかを紹介します。
このセクションはとても長くなります。 というのは、直和型(sum type) とコンビネータから始めることで、Rustにおけるエラーハンドリングを徐々に改善していくための動機を与えるからです。 このような構成ですので、もしすでに他の表現力豊かな型システムの経験があるプログラマでしたら、あちこち拾い読みしたくなるかもしれません。
エラーハンドリングとは、ある処理が成功したかどうかを 場合分け(case analysis) に基づいて判断するものだと考えられます。 これから見ていくように、エラーハンドリングをエルゴノミックにするために重要なのは、プログラマがコードを合成可能(composable) に保ったまま、明示的な場合分けの回数を、いかに減らしていくかということです。
コードを合成可能に保つのは重要です。
なぜなら、もしこの要求がなかったら、想定外のことが起こる度に panic
することを選ぶかもしれないからです。
(panic
は現タスクを巻き戻し(unwind) して、ほとんどの場合、プログラム全体をアボートします。)
// 1から10までの数字を予想します。 // もし予想した数字に一致したらtrueを返し、そうでなければfalseを返します。 fn guess(n: i32) -> bool { if n < 1 || n > 10 { panic!("Invalid number: {}", n); } n == 5 } fn main() { guess(11); }
訳注:文言の意味は
- Invalid number: {}:無効な数字です: {}
ですが、エディタの設定などによっては、ソースコード中の コメント以外の場所に日本語を使うとコンパイルできないことがあるので、 英文のままにしてあります。
このコードを実行すると、プログラムがクラッシュして、以下のようなメッセージが表示されます。
thread '<main>' panicked at 'Invalid number: 11', src/bin/panic-simple.rs:5
次は、もう少し自然な例です。 このプログラムは引数として整数を受け取り、2倍した後に表示します。
use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // エラー1 let n: i32 = arg.parse().unwrap(); // エラー2 println!("{}", 2 * n); }
use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // エラー1 let n: i32 = arg.parse().unwrap(); // エラー2 println!("{}", 2 * n); }
もし、このプログラムに引数を与えなかったら(エラー1)、あるいは、最初の引数が整数でなかったら(エラー2)、このプログラムは、最初の例と同じようにパニックするでしょう。
このようなスタイルのエラーハンドリングは、まるで、陶器店の中を駆け抜ける雄牛のようなものです。 雄牛は自分の行きたいところへたどり着くでしょう。 でも彼は、途中にある、あらゆるものを蹴散らしてしまいます。
先ほどの例で、プログラムが2つのエラー条件のいずれかを満たしたときに、パニックすると言いました。
でもこのプログラムは、最初の例とは違って明示的に panic
を呼び出してはいません。
実はパニックは unwrap
の呼び出しの中に埋め込まれているのです。
Rustでなにかを「アンラップする」とき、こう言っているのと同じです。
「計算結果を取り出しなさい。もしエラーになっていたのなら、パニックを起こしてプログラムを終了させなさい。」
アンラップのコードはとてもシンプルなので、多分、それを見せたほうが早いでしょう。
でもそのためには、まず Option
と Result
型について調べる必要があります。
どちらの型にも unwrap
という名前のメソッドが定義されています。
Option
型Option
型は 標準ライブラリで定義されています:
enum Option<T> { None, Some(T), }
Option
型は、Rustの型システムを使って 不在の可能性 を示すためのものです。
不在の可能性を型システムにエンコードすることは、重要なコンセプトです。
なぜなら、その不在に対処することを、コンパイラがプログラマに強制させるからです。
では、文字列から文字を検索する例を見てみましょう。
fn main() { // Searches `haystack` for the Unicode character `needle`. If one is found, the // byte offset of the character is returned. Otherwise, `None` is returned. // `haystack`(干し草の山)からUnicode文字 `needle`(縫い針)を検索します。 // もし見つかったら、文字のバイトオフセットを返します。見つからなければ、`None` を // 返します。 fn find(haystack: &str, needle: char) -> Option<usize> { for (offset, c) in haystack.char_indices() { if c == needle { return Some(offset); } } None } }
// `haystack`(干し草の山)からUnicode文字 `needle`(縫い針)を検索します。 // もし見つかったら、文字のバイトオフセットを返します。見つからなければ、`None` を // 返します。 fn find(haystack: &str, needle: char) -> Option<usize> { for (offset, c) in haystack.char_indices() { if c == needle { return Some(offset); } } None }
この関数がマッチする文字を見つけたとき、単に offset
を返すだけではないことに注目してください。
その代わりに Some(offset)
を返します。
Some
は Option
型のヴァリアントの一つ、つまり 値コンストラクタ です。
これは fn<T>(value: T) -> Option<T>
という型の関数だと考えることもできます。
これに対応して None
もまた値コンストラクタですが、こちらには引数がありません。
None
は fn<T>() -> Option<T>
という型の関数だと考えることもできます。
何もないことを表すのに、ずいぶん大げさだと感じるかもしれません。
でもこれはまだ、話の半分に過ぎません。
残りの半分は、いま書いた find
関数を 使う 場面です。
これを使って、ファイル名から拡張子を見つけてみましょう。
fn main() { let file_name = "foobar.rs"; match find(file_name, '.') { None => println!("No file extension found."), Some(i) => println!("File extension: {}", &file_name[i+1..]), } }
訳注:
- No file extension found:ファイル拡張子は見つかりませんでした
- File extension: {}:ファイル拡張子:{}
このコードは find
関数が返した Option<usize>
の 場合分け に、 パターンマッチ を使っています。
実のところ、場合分けが、Option<T>
に格納された値を取り出すための唯一の方法なのです。
これは、Option<T>
が Some(t)
ではなく None
だったとき、プログラマであるあなたが、このケースに対処しなければならないことを意味します。
でも、ちょっと待ってください。 さっき 使った unwrap
はどうだったでしょうか?
場合分けはどこにもありませんでした!
実は場合分けは unwrap
メソッドの中に埋め込まれていたのです。
もし望むなら、このように自分で定義することもできます:
fn main() { enum Option<T> { None, Some(T), } impl<T> Option<T> { fn unwrap(self) -> T { match self { Option::Some(val) => val, Option::None => panic!("called `Option::unwrap()` on a `None` value"), } } } }
enum Option<T> { None, Some(T), } impl<T> Option<T> { fn unwrap(self) -> T { match self { Option::Some(val) => val, Option::None => panic!("called `Option::unwrap()` on a `None` value"), } } }
訳注:
called
Option::unwrap()
on aNone
value:
None
な値に対してOption:unwrap()
が呼ばれました
unwrap
メソッドは 場合分けを抽象化します 。このことは確かに unwrap
をエルゴノミックにしています。
しかし残念なことに、そこにある panic!
が意味するものは、unwrap
が合成可能ではない、つまり、陶器店の中の雄牛だということです。
Option<T>
値を合成する先ほどの例 では、ファイル名から拡張子を見つけるために find
をどのように使うかを見ました。
当然ながら全てのファイル名に .
があるわけではなく、拡張子のないファイル名もあり得ます。
このような 不在の可能性 は Option<T>
を使うことによって、型の中にエンコードされています。
すなわち、コンパイラは、拡張子が存在しない可能性に対処することを、私たちに強制してくるわけです。
今回は単に、そうなったことを告げるメッセージを表示するようにしました。
ファイル名から拡張子を取り出すことは一般的な操作ですので、それを関数にすることは理にかなっています。
fn main() { fn find(_: &str, _: char) -> Option<usize> { None } // Returns the extension of the given file name, where the extension is defined // as all characters proceeding the first `.`. // If `file_name` has no `.`, then `None` is returned. // 与えられたファイル名の拡張子を返す。拡張子の定義は、最初の // `.` に続く、全ての文字である。 // もし `file_name` に `.` がなければ、`None` が返される。 fn extension_explicit(file_name: &str) -> Option<&str> { match find(file_name, '.') { None => None, Some(i) => Some(&file_name[i+1..]), } } }// 与えられたファイル名の拡張子を返す。拡張子の定義は、最初の // `.` に続く、全ての文字である。 // もし `file_name` に `.` がなければ、`None` が返される。 fn extension_explicit(file_name: &str) -> Option<&str> { match find(file_name, '.') { None => None, Some(i) => Some(&file_name[i+1..]), } }
(プロ向けのヒント:このコードは使わず、代わりに標準ライブラリの
extension
メソッドを使ってください)
このコードはいたってシンプルですが、ひとつだけ注目して欲しいのは、find
の型が不在の可能性について考慮することを強制していることです。
これは良いことです。なぜなら、コンパイラが私たちに、ファイル名が拡張子を持たないケースを、うっかり忘れないようにしてくれるからです。
しかし一方で、 extension_explicit
でしたような明示的な場合分けを毎回続けるのは、なかなか面倒です。
実は extension_explicit
での場合分けは、ごく一般的なパターンである、Option<T>
への map の適用に当てはめられます。
これは、もしオプションが None
なら None
を返し、そうでなけれは、オプションの中の値に関数を適用する、というパターンです。
Rustはパラメトリック多相をサポートしていますので、このパターンを抽象化するためのコンビネータが簡単に定義できます:
fn main() { fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A { match option { None => None, Some(value) => Some(f(value)), } } }
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A { match option { None => None, Some(value) => Some(f(value)), } }
もちろん map
は、標準のライブラリの Option<T>
で メソッドとして定義されています。
メソッドなので、少し違うシグネチャを持っています。
メソッドは第一引数に self
、 &self
あるいは &mut self
を取ります。
新しいコンビネータを手に入れましたので、 extension_explicit
メソッドを書き直して、場合分けを省きましょう:
// 与えられたファイル名の拡張子を返す。拡張子の定義は、最初の // `.` に続く、全ての文字である。 // もし `file_name` に `.` がなければ、`None` が返される。 fn extension(file_name: &str) -> Option<&str> { find(file_name, '.').map(|i| &file_name[i+1..]) }
もう一つの共通のパターンは、Option
の値が None
だったときのデフォルト値を与えることです。
例えばファイルの拡張子がないときは、それを rs
とみなすようなプログラムを書きたくなるかもしれません。
ご想像の通り、このような場合分けはファイルの拡張子に特有のものではありません。
どんな Option<T>
でも使えるでしょう:
fn unwrap_or<T>(option: Option<T>, default: T) -> T { match option { None => default, Some(value) => value, } }
上記の map
と同じように、標準ライブラリの実装はただの関数ではなくメソッドになっています。
ここでの仕掛けは、Option<T>
に入れる値と同じ型になるよう、デフォルト値の型を制限していることです。
これを使うのは、すごく簡単です:
fn main() { assert_eq!(extension("foobar.csv").unwrap_or("rs"), "csv"); assert_eq!(extension("foobar").unwrap_or("rs"), "rs"); }
(unwrap_or
は、標準のライブラリの Option<T>
で、 メソッドとして定義されています ので、いま定義したフリースタンディングな関数の代わりに、そちらを使いましょう。)
もうひとつ特筆すべきコンビネータがあります。それは and_then
です。これを使うと 不在の可能性 を考慮しながら、別々の処理を簡単に組み合わせることができます。
例えば、この節のほとんどのコードは、与えられたファイル名について拡張子を見つけだします。
そのためには、まずファイル パス から取り出したファイル名が必要です。
大抵のパスにはファイル名がありますが、 全て がというわけではありません。
例えば .
, ..
, /
などは例外です。
つまり、与えられたファイル パス から拡張子を見つけ出せるか、トライしなければなりません。 まず明示的な場合分けから始めましょう:
fn main() { fn extension(file_name: &str) -> Option<&str> { None } fn file_path_ext_explicit(file_path: &str) -> Option<&str> { match file_name(file_path) { None => None, Some(name) => match extension(name) { None => None, Some(ext) => Some(ext), } } } fn file_name(file_path: &str) -> Option<&str> { // implementation elided // 実装は省略 unimplemented!() } }fn file_path_ext_explicit(file_path: &str) -> Option<&str> { match file_name(file_path) { None => None, Some(name) => match extension(name) { None => None, Some(ext) => Some(ext), } } } fn file_name(file_path: &str) -> Option<&str> { // 実装は省略 unimplemented!() }
場合分けを減らすために単に map
コンビネータを使えばいいと思うかもしれませんが、型にうまく適合しません。
fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).map(|x| extension(x)) //Compilation error }
ここの map
関数は Option<_>
内で extension
関数が返した値をラップしていますが extension
関数自身が Option<&str>
を返すので、式 file_name(file_path).map(|x| extension(x))
は実際は Option<Option<&str>>
を返すのです。
しかしながら file_path_ext
は(Option<Option<&str>>
ではなく)ただの Option<&str>
を返すのでコンパイルエラーとなるのです。
そして関数が返した値は 必ず Some
でラップされ直します 。
つまりこの代わりに、 map
に似た、しかし新たに Option<_>
で包まずに直接呼び出し元に常に Option<_>
を返すものが必要です。
これの汎用的な実装は map
よりもシンプルです:
fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> Option<A> { match option { None => None, Some(value) => f(value), } }
では、明示的な場合分けを省くように、 file_path_ext
を書き直しましょう:
fn file_path_ext(file_path: &str) -> Option<&str> { file_name(file_path).and_then(extension) }
補足: and_then
は基本的に map
のように振舞いますが Option<Option_>>
の代わりに Option<_>
を返すので flatmap
と呼ぶ言語もあります。
Option
型には、他にもたくさんのコンビネータが 標準ライブラリで定義されています 。
それらの一覧をざっと眺めて、なにがあるか知っておくといいでしょう。
大抵の場合、場合分けを減らすのに役立ちます。
それらのコンビネータに慣れるための努力は、すぐに報われるでしょう。
なぜなら、そのほとんどは次に話す Result
型でも、(よく似たセマンティクスで)定義されているからです。
コンビネータは明示的な場合分けを減らしてくれるので、 Option
のような型をエルゴノミックにします。
またこれらは 不在の可能性 を、呼び出し元がそれに合った方法で扱えるようにするので、合成可能だといえます。
unwrap
のようなメソッドは、 Option<T>
が None
のときにパニックを起こすので、このような選択の機会を与えません。
Result
型Result
型も 標準ライブラリで定義されています 。
fn main() { enum Result<T, E> { Ok(T), Err(E), } }
enum Result<T, E> { Ok(T), Err(E), }
Result
型は Option
型の豪華版です。
Option
のように 不在 の可能性を示す代わりに、Result
は エラー になる可能性を示します。
通常 エラー は、なぜ処理が実行に失敗したのかを説明するために用いられます。
これは厳密には Option
をさらに一般化した形式だといえます。
以下のような型エイリアスがあるとしましょう。
これは全てにおいて、本物の Option<T>
と等しいセマンティクスを持ちます。
type Option<T> = Result<T, ()>;
これは Result
の2番目の型パラメータを ()
(「ユニット」または「空タプル」と発音します)に固定したものです。
()
型のただ一つの値は ()
です。
(そうなんです。型レベルと値レベルの項が、全く同じ表記法を持ちます!)
Result
型は、処理の結果がとりうる2つの可能性のうち、1つを表すための方法です。
慣例に従い、一方が期待されている結果、つまり「Ok
」となり、もう一方が予想外の結果、つまり「Err
」になります。
Option
と全く同じように、Result
型も標準ライブラリで unwrap
メソッドが定義されています 。
定義してみましょう:
impl<T, E: ::std::fmt::Debug> Result<T, E> { fn unwrap(self) -> T { match self { Result::Ok(val) => val, Result::Err(err) => panic!("called `Result::unwrap()` on an `Err` value: {:?}", err), } } }
訳注:
called
Result::unwrap()
on anErr
value: {:?}":
Err
値 {:?} に対してResult::unwrap()
が呼ばれました
これは実質的には私たちの Option::unwrap
の定義 と同じですが、 panic!
メッセージにエラーの値が含まれているところが異なります。
これはデバッグをより簡単にしますが、一方で、(エラーの型を表す)型パラメータ E
に Debug
制約を付けることが求められます。
大半の型は Debug
制約を満たしているので、実際のところ、うまくいく傾向にあります。
(Debug
が型に付くということは、単にその型の値が、人間が読める形式で表示できることを意味しています。)
では、例を見ていきましょう。
Rustの標準ライブラリを使うと、文字列を整数に変換することが、すごく簡単にできます。 あまりにも簡単なので、実際のところ、以下のように書きたいという誘惑に負けることがあります:
fn double_number(number_str: &str) -> i32 { 2 * number_str.parse::<i32>().unwrap() } fn main() { let n: i32 = double_number("10"); assert_eq!(n, 20); }fn double_number(number_str: &str) -> i32 { 2 * number_str.parse::<i32>().unwrap() } fn main() { let n: i32 = double_number("10"); assert_eq!(n, 20); }
すでにあなたは、unwrap
を呼ぶことについて懐疑的になっているはずです。
例えば、文字列が数字としてパースできなければ、パニックが起こります。
thread '<main>' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', /home/rustbuild/src/rust-buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729
これは少し目障りです。
もしあなたが使っているライブラリの中でこれが起こされたら、イライラするに違いありません。
代わりに、私たちの関数の中でエラーを処理し、呼び出し元にどうするのかを決めさせるべきです。
そのためには、double_number
の戻り値の型(リターン型)を変更しなければなりません。
でも、一体何に?
ええと、これはつまり、標準ライブラリの parse
メソッド のシグネチャを見ろということです。
impl str { fn parse<F: FromStr>(&self) -> Result<F, F::Err>; }
うむ。最低でも Result
を使わないといけないことはわかりました。
もちろん、これが Option
を戻すようにすることも可能だったでしょう。
結局のところ、文字列が数字としてパースできたかどうかが知りたいわけですよね?
それも悪いやり方ではありませんが、実装の内側では なぜ 文字列が整数としてパースできなかったを、ちゃんと区別しています。
(空の文字列だったのか、有効な数字でなかったのか、大きすぎたり、小さすぎたりしたのか。)
従って、Result
を使ってより多くの情報を提供するほうが、単に「不在」を示すことよりも理にかなっています。
今後、もし Option
と Result
のどちらを選ぶという事態に遭遇したときは、このような理由付けのやり方を真似てみてください。
もし詳細なエラー情報を提供できるのなら、多分、それをしたほうがいいでしょう。
(後ほど別の例もお見せます。)
それでは、リターン型をどう書きましょうか?
上の parse
メソッドは一般化されているので、標準ライブラリにある、あらゆる数値型について定義されています。
この関数を同じように一般化することもできますが(そして、そうするべきでしょうが)、今は明快さを優先しましょう。
i32
だけを扱うことにしますので、それの FromStr
の実装がどうなっているか探しましょう 。
(ブラウザで CTRL-F
を押して「FromStr」を探します。)
そして 関連型(associated type) から Err
を見つけます。
こうすれば、具体的なエラー型が見つかります。
この場合、それは std::num::ParseIntError
です。
これでようやく関数を書き直せます:
use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { match number_str.parse::<i32>() { Ok(n) => Ok(2 * n), Err(err) => Err(err), } } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } }
これで少し良くなりましたが、たくさんのコードを書いてしまいました! 場合分けに、またしてもやられたわけです。
コンビネータに助けを求めましょう!
ちょうど Option
と同じように Result
にもたくさんのコンビネータが、メソッドとして定義されています。
Result
と Option
の間では、共通のコンビネータが数多く存在します。
例えば map
も共通なものの一つです:
use std::num::ParseIntError; fn double_number(number_str: &str) -> Result<i32, ParseIntError> { number_str.parse::<i32>().map(|n| 2 * n) } fn main() { match double_number("10") { Ok(n) => assert_eq!(n, 20), Err(err) => println!("Error: {:?}", err), } }
Result
でいつも候補にあがるのは unwrap_or
と and_then
です。
さらに Result
は2つ目の型パラメータを取りますので、エラー型だけに影響を与える map_err
(map
に相当)と or_else
(and_then
に相当)もあります。
Result
型エイリアスを用いたイディオム標準ライブラリでは Result<i32>
のような型をよく見ると思います。
でも、待ってください。
2つの型パラメータを取るように Result
を定義したはずです 。
どうして、1つだけを指定して済んだのでしょう?
種を明かすと、Result
の型エイリアスを定義して、一方の型パラメータを特定の型に 固定 したのです。
通常はエラー型の方を固定します。
例えば、先ほどの整数のパースの例は、こう書き換えることもできます。
use std::num::ParseIntError; use std::result; type Result<T> = result::Result<T, ParseIntError>; fn double_number(number_str: &str) -> Result<i32> { unimplemented!(); }
なぜ、こうするのでしょうか?
もし ParseIntError
を返す関数をたくさん定義するとしたら、常に ParseIntError
を使うエイリアスを定義したほうが便利だからです。
こうすれば、同じことを何度も書かずに済みます。
標準ライブラリで、このイディオムが際立って多く使われている場所では、io::Result
を用いています。
それらは通常 io::Result<T>
のように書かれ、std::result
のプレーンな定義の代わりに io
モジュールの型エイリアスを使っていることが、明確にわかるようになっています。
これまでの説明を読んだあなたは、 unwrap
のような panic
を起こし、プログラムをアボートするようなメソッドについて、私がきっぱりと否定する方針をとっていたことに気づいたかもしれません。
一般的には これは良いアドバイスです。
しかしながら unwrap
を使うのが賢明なこともあります。
どんな場合に unwrap
の使用を正当化できるのかについては、グレーな部分があり、人によって意見が分かれます。
ここで、この問題についての、私の 個人的な意見 をまとめたいと思います。
unwrap
の便利さは、とても魅力的に映るでしょう。
これに打ち勝つのは難しいことです。assert!
の失敗のような明示的な要因によるものだったり、配列のインデックスが境界から外れたからだったりします。これは多分、完全なリストではないでしょう。
さらに Option
を使うときは、ほとんどの場合で expect
メソッドを使う方がいいでしょう。
expect
は unwrap
とほぼ同じことをしますが、 expect
では与えられたメッセージを表示するところが異なります。
この方が結果として起こったパニックを、少し扱いやすいものにします。
なぜなら「 None
な値に対してアンラップが呼ばれました」というメッセージの代わりに、指定したメッセージが表示されるからです。
私のアドバイスを突き詰めると、よく見極めなさい、ということです。 私の書いた文章の中に「決して、Xをしてはならない」とか「Yは有害だと考えよう」といった言葉が現れないのには、れっきとした理由があります。 あるユースケースでこれが容認できるかどうかは、プログラマであるあなたの判断に委ねられます。 私が目指していることは、あなたがトレードオフをできるかぎり正確に評価できるよう、手助けをすることなのです。
これでRustにおけるエラーハンドリングの基礎をカバーできました。 また、アンラップについても解説しました。 では標準ライブラリをもっと探索していきましょう。
これまで見てきたエラーハンドリングでは、 Option<T>
または Result<T, SomeError>
が1つあるだけでした。
ではもし Option
と Result
の両方があったらどうなるでしょうか?
あるいは、Result<T, Error1>
と Result<T, Error2>
があったら?
異なるエラー型の組み合わせ を扱うことが、いま目の前にある次なる課題です。
またこれが、このセクションの残りの大半に共通する、主要なテーマとなります。
Option
と Result
を合成するこれまで話してきたのは Option
のために定義されたコンビネータと、 Result
のために定義されたコンビネータについてでした。
これらのコンビネータを使うと、様々な処理の結果を明示的な場合分けなしに組み合わせることができました。
もちろん現実のコードは、いつもこんなにクリーンではありません。
時には Option
型と Result
型が混在していることもあるでしょう。
そんなときは、明示的な場合分けに頼るしかないのでしょうか?
それとも、コンビネータを使い続けることができるのでしょうか?
ここで、このセクションの最初の方にあった例に戻ってみましょう:
use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // エラー1 let n: i32 = arg.parse().unwrap(); // エラー2 println!("{}", 2 * n); }use std::env; fn main() { let mut argv = env::args(); let arg: String = argv.nth(1).unwrap(); // エラー1 let n: i32 = arg.parse().unwrap(); // エラー2 println!("{}", 2 * n); }
これまでに獲得した知識、つまり Option
、Result
と、それらのコンビネータに関する知識を動員して、これを書き換えましょう。
エラーを適切に処理し、もしエラーが起こっても、プログラムがパニックしないようにするのです。
ここでの問題は argv.nth(1)
が Option
を返すのに、 arg.parse()
は Result
を返すことです。
これらを直接合成することはできません。
Option
と Result
の両方に出会ったときの 通常の 解決策は Option
を Result
に変換することです。
この例で(env::args()
が)コマンドライン引数を返さなかったということは、ユーザーがプログラムを正しく起動しなかったことを意味します。
エラーの理由を示すために、 String
を使うこともできます。
試してみましょう:
use std::env; fn double_arg(mut argv: env::Args) -> Result<i32, String> { argv.nth(1) .ok_or("Please give at least one argument".to_owned()) .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string())) .map(|n| 2 * n) } fn main() { match double_arg(env::args()) { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
use std::env; fn double_arg(mut argv: env::Args) -> Result<i32, String> { argv.nth(1) .ok_or("Please give at least one argument".to_owned()) .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string())) .map(|n| 2 * n) } fn main() { match double_arg(env::args()) { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
訳注:
Please give at least one argument:引数を最低1つ指定してください。
この例では、いくつか新しいことがあります。
ひとつ目は Option::ok_or
コンビネータを使ったことです。
これは Option
を Result
へ変換する方法の一つです。
変換には Option
が None
のときに使われるエラーを指定する必要があります。
他のコンビネータと同様に、その定義はとてもシンプルです:
fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> { match option { Some(val) => Ok(val), None => Err(err), } }
ここで使った、もう一つの新しいコンビネータは Result::map_err
です。
これは Result::map
に似ていますが、 Result
値の エラー の部分に対して関数をマップするところが異なります。
もし Result
の値が Ok(...)
だったら、そのまま変更せずに返します。
map_err
を使った理由は、(and_then
の用法により)エラーの型を同じに保つ必要があったからです。
ここでは(argv.nth(1)
が返した) Option<String>
を Result<String, String>
に変換することを選んだため、arg.parse()
が返した ParseIntError
も String
に変換しなければならなかったわけです。
入出力と共に入力をパースすることは、非常によく行われます。 そして私がRustを使って個人的にやってきたことのほとんども、これに該当しています。 ですから、ここでは(そして、この後も) IOと様々なパースを行うルーチンを、エラーハンドリングの例として扱っていきます。
まずは簡単なものから始めましょう。
ここでのタスクは、ファイルを開き、その内容を全て読み込み、1つの数値に変換することです。
そしてそれに 2
を掛けて、結果を表示します。
いままで unwrap
を使わないよう説得してきたわけですが、最初にコードを書くときには unwrap
が便利に使えます。
こうすることで、エラーハンドリングではなく、本来解決すべき課題に集中できます。
それと同時に unwrap
は、適切なエラーハンドリングが必要とされる場所を教えてくれます。
ここから始めることをコーディングへの取っ掛かりとしましょう。
その後、リファクタリングによって、エラーハンドリングを改善していきます。
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> i32 { let mut file = File::open(file_path).unwrap(); // エラー1 let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); // エラー2 let n: i32 = contents.trim().parse().unwrap(); // エラー3 2 * n } fn main() { let doubled = file_double("foobar"); println!("{}", doubled); }
(備考: AsRef<Path>
を使ったのは、std::fs::File::open
で使われているものと同じ境界 だからです。
ファイルパスとして、どんな文字列でも受け付けるので、エルゴノミックになります。)
ここでは3種類のエラーが起こる可能性があります:
最初の2つの問題は、std::io::Error
型で記述されます。
これは std::fs::File::open
と std::io::Read::read_to_string
のリターン型からわかります。
(ちなみにどちらも、以前紹介した Result
型エイリアスのイディオム を用いています。
Result
型のところをクリックすると、いま言った 型エイリアスを見たり、必然的に、中で使われている io::Error
型も見ることになるでしょう。)
3番目の問題は std::num::ParseIntError
型で記述されます。
特にこの io::Error
型は標準ライブラリ全体に 深く浸透しています 。
これからこの型を幾度となく見ることでしょう。
まず最初に file_double
関数をリファクタリングしましょう。
この関数を、このプログラムの他の構成要素と合成可能にするためには、上記の問題のいずれかに遭遇しても、パニック しない ようにしなければなりません。
これは実質的には、なにかの操作に失敗したときに、この関数が エラーを返すべき であることを意味します。
ここでの問題は、file_double
のリターン型が i32
であるため、エラーの報告には全く役立たないことです。
従ってリターン型を i32
から別の何かに変えることから始めましょう。
最初に決めるべきことは、 Option
と Result
のどちらを使うかです。
Option
なら間違いなく簡単に使えます。
もし3つのエラーのどれかが起こったら、単に None
を返せばいいのですから。
これはたしかに動きますし、 パニックを起こすよりは良くなっています 。
とはいえ、もっと良くすることもできます。
Option
の代わりに、発生したエラーについての詳細を渡すべきでしょう。
ここでは エラーの可能性 を示したいのですから、Result<i32, E>
を使うのがよさそうです。
でも E
を何にしたらいいのでしょうか?
2つの 異なる 型のエラーが起こり得ますので、これらを共通の型に変換する必要があります。
そのような型の一つに String
があります。
この変更がコードにどんな影響を与えるか見てみましょう:
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { File::open(file_path) .map_err(|err| err.to_string()) .and_then(|mut file| { let mut contents = String::new(); file.read_to_string(&mut contents) .map_err(|err| err.to_string()) .map(|_| contents) }) .and_then(|contents| { contents.trim().parse::<i32>() .map_err(|err| err.to_string()) }) .map(|n| 2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
このコードは、やや難解になってきました。
このようなコードを簡単に書けるようになるまでには、結構な量の練習が必要かもしれません。
こういうものを書くときは 型に導かれる ようにします。
file_double
のリターン型を Result<i32, String>
に変更したらすぐに、それに合ったコンビネータを探し始めるのです。
この例では and_then
, map
, map_err
の、3種類のコンビネータだけを使いました。
and_then
は、エラーを返すかもしれない処理同士を繋いでいくために使います。
ファイルを開いた後に、失敗するかもしれない処理が2つあります:
ファイルから読み込む所と、内容を数値としてパースする所です。
これに対応して and_then
も2回呼ばれています。
map
は Result
の値が Ok(...)
のときに関数を適用するために使います。
例えば、一番最後の map
の呼び出しは、Ok(...)
の値( i32
型)に 2
を掛けます。
もし、これより前にエラーが起きたなら、この操作は map
の定義に従ってスキップされます。
map_err
は全体をうまく動かすための仕掛けです。
map_err
は map
に似ていますが、 Result
の値が Err(...)
のときに関数を適用するところが異なります。
今回の場合は、全てのエラーを String
という同一の型に変換する予定でした。
io::Error
と num::ParseIntError
の両方が ToString
を実装していたので、 to_string()
メソッドを呼ぶことで変換できました。
説明し終わった後でも、このコードは難解なままです。 コンビネータの使い方をマスタすることは重要ですが、コンビネータには限界もあるのです。 次は、早期リターンと呼ばれる、別のアプローチを試してみましょう。
前の節で使ったコードを、 早期リターン を使って書き直してみようと思います。
早期リターンとは、関数の途中で抜けることを指します。
file_double
のクロージャの中にいる間は、早期リターンはできないので、明示的な場合分けまでいったん戻る必要があります。
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = match File::open(file_path) { Ok(file) => file, Err(err) => return Err(err.to_string()), }; let mut contents = String::new(); if let Err(err) = file.read_to_string(&mut contents) { return Err(err.to_string()); } let n: i32 = match contents.trim().parse() { Ok(n) => n, Err(err) => return Err(err.to_string()), }; Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
このコードが、コンビネータを使ったコードよりも良くなったのかについては、人によって意見が分かれるでしょう。
でも、もしあなたがコンビネータによるアプローチに不慣れだったら、このコードのほうが読みやすいと思うかもしれません。
ここでは明示的な場合分けを match
と if let
で行っています。
もしエラーが起きたら関数の実行を打ち切って、エラーを(文字列に変換してから)返します。
でもこれって逆戻りしてませんか? 以前は、エラーハンドリングをエルゴノミックにするために、明示的な場合分けを減らすべきだと言っていました。 それなのに、今は明示的な場合分けに戻ってしまっています。 すぐにわかりますが、明示的な場合分けを減らす方法は 複数 あるのです。 コンビネータが唯一の方法ではありません。
try!
マクロRustでのエラー処理の基礎となるのは try!
マクロです。
try!
マクロはコンビネータと同様、場合分けを抽象化します。
しかし、コンビネータと異なるのは 制御フロー も抽象化してくれることです。
つまり、先ほど見た 早期リターン のパターンを抽象化できるのです。
try!
マクロの簡略化した定義はこうなります:
fn main() { macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); } }
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); }
(本当の定義 はもっと洗練されています。 後ほど紹介します。)
try!
マクロを使うと、最後の例をシンプルにすることが、とても簡単にできます。
場合分けと早期リターンを肩代わりしてくれますので、コードが締まって読みやすくなります。
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!("Error: {}", err), } }
今の私たちの try!
の定義 ですと、 map_err
は今でも必要です。
なぜなら、エラー型を String
に変換しなければならないからです。
でも、いい知らせがあります。
map_err
の呼び出しを省く方法をすぐに習うのです!
悪い知らせは、map_err
を省く前に、標準ライブラリのいくつかの重要なトレイトについて、もう少し学ぶ必要があるということです。
標準ライブラリのいくつかのエラートレイトについて学ぶ前に、これまでの例にあったエラー型における String
の使用を取り除くことで、この節を締めくくりたいと思います。
これまでの例では String
を便利に使ってきました。
なぜなら、エラーは簡単に文字列へ変換できますし、問題が起こったその場で、文字列によるエラーを新たに作ることもできるからです。
しかし String
を使ってエラーを表すことには欠点もあります。
ひとつ目の欠点は、エラーメッセージがコードのあちこちに散らかる傾向があることです。 エラーメッセージをどこか別の場所でまとめて定義することもできますが、特別に訓練された人でない限りは、エラーメッセージをコードに埋め込むことへの誘惑に負けてしまうでしょう。 実際、私たちは 以前の例 でも、その通りのことをしました。
ふたつ目の、もっと重大な欠点は、 String
への変換で 情報が欠落する ことです。
もし全てのエラーを文字列に変換してしまったら、呼び出し元に渡したエラーが、オペーク(不透明)になってしまいます。
呼び出し元が String
のエラーに対してできる唯一妥当なことは、それをユーザーに表示することだけです。
文字列を解析して、どのタイプのエラーだったか判断するのは、もちろん強固なやり方とはいえません。
(この問題は、ライブラリの中の方が、他のアプリケーションのようなものよりも、間違いなく重大なものになるでしょう。)
例えば io::Error
型には io::ErrorKind
が埋め込まれます。
これは 構造化されたデータ で、IO操作において何が失敗したのかを示します。
エラーによって違った対応を取りたいこともあるので、このことは重要です。
(例: あなたのアプリケーションでは BrokenPipe
エラーは正規の手順を踏んだ終了を意味し、 NotFound
エラーはエラーコードと共に異常終了して、ユーザーにエラーを表示することを意味するかもしれません。)
io::ErrorKind
なら、呼び出し元でエラーの種類を調査するために、場合分けが使えます。
これは String
の中からエラーの詳細がなんだったのか探りだすことよりも、明らかに優れています。
ファイルから整数値を取り出す例で String
をエラー型として用いた代わりに、独自のエラー型を定義し、 構造化されたデータ によってエラー内容を表すことができます。
呼び出し元が詳細を検査したいときに備え、大元のエラーについての情報を取りこぼさないよう、努力してみましょう。
多くの可能性のうちの一つ を表す理想的な方法は、 enum
を使って独自の直和型を定義することです。
このケースでは、エラーは io::Error
もしくは num::ParseIntError
でした。
ここから思い浮かぶ自然な定義は:
use std::io; use std::num; // 全ての型は `Debug` を導出するべきでしょうから、ここでも `Debug` を導出します。 // これにより `CliError` 値について、人間が十分理解できる説明を得られます。 #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), }
コードの微調整はいとも簡単です。
エラーを文字列に変換する代わりに、エラーに対応する値コンストラクタを用いて CliError
型に変換すればいいのです:
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } fn main() { match file_double("foobar") { Ok(n) => println!("{}", n), Err(err) => println!("Error: {:?}", err), } }
ここでの変更点は、(エラーを文字列に変換する) map_err(|e| e.to_string())
を、map_err(CliError::Io)
や map_err(CliError::Parse)
へ切り替えたことです。
こうして 呼び出し元 が、ユーザーに対してどの程度の詳細を報告するか決められるようになりました。
String
をエラー型として用いることは、事実上、呼び出し元からこうした選択肢を奪ってしまいます。
CliError
のような独自の enum
エラー型を用いることは、 構造化されたデータ によるエラーの説明だけでなく、これまでと同様の使いやすさをもたらします。
目安となる方法は独自のエラー型を定義することですが、 String
エラー型も、いざというときに役立ちます。
特にアプリケーションを書いているときなどはそうです。
もしライブラリを書いているのなら、呼び出し元の選択肢を理由もなく奪わないために、独自のエラー型を定義することを強く推奨します。
標準ライブラリでは、エラーハンドリングに欠かせないトレイトが、2つ定義されています:
std::error::Error
と std::convert::From
です。
Error
はエラーを総称的に説明することを目的に設計されているのに対し、 From
トレイトはもっと汎用的な、2つの異なる型の間で値を変換する役割を担います。
Error
トレイトError
トレイトは 標準ライブラリで定義されています :
use std::fmt::{Debug, Display}; trait Error: Debug + Display { /// エラーの簡単な説明 fn description(&self) -> &str; /// このエラーの一段下のレベルの原因(もしあれば) fn cause(&self) -> Option<&Error> { None } }
このトレイトは極めて一般的です。 なぜなら、エラーを表す 全て の型で実装されることを目的としているからです。 この後すぐ見るように、このことは合成可能なコードを書くのに間違いなく役立ちます。 それ以外にも、このトレイトでは最低でも以下のようなことができます:
Debug
表現を取得する。Display
表現を取得する。description
メソッド経由)。cause
メソッド経由)。最初の2つは Error
が Debug
と Display
の実装を必要としていることに由来します。
残りの2つは Error
が定義している2つのメソッドに由来します。
Error
の強力さは、実際に全てのエラー型が Error
を実装していることから来ています。
このことは、全てのエラーを1つの トレイトオブジェクト として存在量化(existentially quantify) できることを意味します。
これは Box<Error>
または &Error
と書くことで表明できます。
実際に cause
メソッドは &Error
を返し、これ自体はトレイトオブジェクトです。
Error
トレイトのトレイトオブジェクトとしての用例については、後ほど再び取りあげます。
Error
トレイトの実装例を見せるには、いまはこのくらいで十分でしょう。
前の節 で定義したエラー型を使ってみましょう:
use std::io; use std::num; // 全ての型は `Debug` を導出するべきでしょうから、ここでも `Debug` を導出します。 // これにより `CliError` 値について、人間が十分理解できる説明を得られます。 #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), }
このエラー型は2種類のエラー、つまり、IOを扱っているときのエラー、または、文字列を数値に変換するときのエラーが起こる可能性を示しています。
enum
定義のヴァリアントを増やせば、エラーの種類をいくらでも表現できます。
Error
を実装するのは実に単純な作業です。
大抵は明示的な場合分けの繰り返しになります。
use std::error; use std::fmt; impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { // 下層のエラーは両方ともすでに `Display` を実装しているので、 // それらの実装に従います。 CliError::Io(ref err) => write!(f, "IO error: {}", err), CliError::Parse(ref err) => write!(f, "Parse error: {}", err), } } } impl error::Error for CliError { fn description(&self) -> &str { // 下層のエラーは両方ともすでに `Error` を実装しているので、 // それらの実装に従います。 match *self { CliError::Io(ref err) => err.description(), CliError::Parse(ref err) => err.description(), } } fn cause(&self) -> Option<&error::Error> { match *self { // 注意:これらは両方とも `err` を、その具象型(`&io::Error` か // `&num::ParseIntError` のいずれか)から、トレイトオブジェクト // `&Error` へ暗黙的にキャストします。どちらのエラー型も `Error` を // 実装しているので、問題なく動きます。 CliError::Io(ref err) => Some(err), CliError::Parse(ref err) => Some(err), } } }
これは極めて典型的な Error
の実装だということに留意してください。
このように、それぞれのエラー型にマッチさせて、description
と cause
のコントラクトを満たします。
From
トレイトstd::convert::From
は 標準ライブラリで定義されています :
fn main() { trait From<T> { fn from(T) -> Self; } }
trait From<T> { fn from(T) -> Self; }
嬉しいくらい簡単でしょ?
From
は、ある特定の T
という型 から 、別の型へ変換するための汎用的な方法を提供するので大変便利です
(この場合の「別の型」とは実装の主体、つまり Self
です)。
From
を支えているのは 標準ライブラリで提供される一連の実装です。
From
がどのように動くか、いくつかの例を使って紹介しましょう:
let string: String = From::from("foo"); let bytes: Vec<u8> = From::from("foo"); let cow: ::std::borrow::Cow<str> = From::from("foo");
たしかに From
が文字列を変換するのに便利なことはわかりました。
でもエラーについてはどうでしょうか?
結論から言うと、これが重要な実装です:
impl<'a, E: Error + 'a> From<E> for Box<Error + 'a>
この実装では、 Error
を実装した 全て の型は、トレイトオブジェクト Box<Error>
に変換できると言っています。
これは、驚きに値するものには見えないかもしれませんが、一般的なコンテキストで有用なのです。
さっき扱った2つのエラーを覚えてますか?
具体的には io::Error
と num::ParseIntError
でした。
どちらも Error
を実装していますので From
で動きます。
use std::error::Error; use std::fs; use std::io; use std::num; // エラーの値にたどり着くまで、何段階かのステップが必要です。 let io_err: io::Error = io::Error::last_os_error(); let parse_err: num::ParseIntError = "not a number".parse::<i32>().unwrap_err(); // では、こちらで変換します。 let err1: Box<Error> = From::from(io_err); let err2: Box<Error> = From::from(parse_err);
ここに気づくべき、本当に重要なパターンがあります。
err1
と err2
の両方ともが 同じ型 になっているのです。
なぜなら、それらが存在量化型、つまり、トレイトオブジェクトだからです。
特にそれらの背後の型は、コンパイラの知識から 消去されます ので、 err1
と err2
が本当に同じに見えるのです。
さらに私たちは同じ関数呼び出し From::from
を使って err1
と err2
をコンストラクトしました。
これは From::from
が引数とリターン型の両方でオーバーロードされているからです。
このパターンは重要です。 なぜなら、私たちが前から抱えていた問題を解決するからです: 同じ関数を使って、エラーを同一の型に変換する、確かな方法を提供するからです。
いよいよ、私たちの旧友 try!
マクロを再訪するときが訪れました。
try!
マクロtry!
の定義は、以前このように提示されました:
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(err), }); }
これは本当の定義ではありません。 本当の定義は 標準ライブラリの中にあります:
fn main() { macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); } }
macro_rules! try { ($e:expr) => (match $e { Ok(val) => val, Err(err) => return Err(::std::convert::From::from(err)), }); }
文面上はわずかですが、非常に大きな違いがあります:
エラーの値は From::from
を経て渡されるのです。
これにより try!
マクロは、はるかに強力になります。
なぜなら、自動的な型変換をただで手に入れられるのですから。
強力になった try!
マクロを手に入れたので、以前書いた、ファイルを読み込んで内容を整数値に変換するコードを見直してみましょう:
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); Ok(2 * n) }
以前 map_err
の呼び出しを取り除くことができると約束しました。
もちろんです。ここでしなければいけないのは From
と共に動く型を一つ選ぶだけでよいのです。
前の節で見たように From
の実装の一つは、どんなエラー型でも Box<Error>
に変換できます:
use std::error::Error; use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n = try!(contents.trim().parse::<i32>()); Ok(2 * n) }
理想的なエラーハンドリングまで、あと一歩です。
私たちのコードには、エラーハンドリングを終えた後も、ごくわずかなオーバーヘッドしかありません。
これは try!
マクロが同時に3つのことをカプセル化するからです:
これら3つが一つになったとき、コンビネータ、 unwrap
の呼び出し、場合分けなどの邪魔者を排除したコードが得られるのです。
あとひとつ、些細なことが残っています:
Box<Error>
型は オペーク なのです。
もし Box<Error>
を呼び出し元に返すと、呼び出し元では背後のエラー型が何であるかを、(簡単には)調べられません。
この状況は String
を返すよりは明らかに改善されてます。
なぜなら、呼び出し元では description
や cause
といったメソッドを呼ぶこともできるからです。
しかし Box<Error>
が不透明であるという制限は残ります。
(注意:これは完全な真実ではありません。
なぜならRustでは実行時のリフレクションができるからです。
この方法が有効なシナリオもありますが、このセクションで扱う範囲を超えています )
では、私たちの独自のエラー型 CliError
に戻って、全てを一つにまとめ上げましょう。
前の節では try!
マクロの本当の定義を確認し、それが From::from
をエラーの値に対して呼ぶことで、自動的な型変換をする様子を見ました。
特にそこでは、エラーを Box<Error>
に変換しました。
これはたしかに動きますが、呼び出し元にとって、型がオペークになってしまいました。
これを直すために、すでによく知っている改善方法である独自のエラー型を使いましょう。 もう一度、ファイルの内容を読み込んで整数値に変換するコードです:
fn main() { use std::fs::File; use std::io::{self, Read}; use std::num; use std::path::Path; // We derive `Debug` because all types should probably derive `Debug`. // This gives us a reasonable human readable description of `CliError` values. // 全ての型は `Debug` を導出するべきでしょうから、ここでも `Debug` を導出します。 // これにより `CliError` 値について、人間が十分理解できる説明を得られます。 #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) } }use std::fs::File; use std::io::{self, Read}; use std::num; use std::path::Path; // 全ての型は `Debug` を導出するべきでしょうから、ここでも `Debug` を導出します。 // これにより `CliError` 値について、人間が十分理解できる説明を得られます。 #[derive(Debug)] enum CliError { Io(io::Error), Parse(num::ParseIntError), } fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path).map_err(CliError::Io)); let mut contents = String::new(); try!(file.read_to_string(&mut contents).map_err(CliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse)); Ok(2 * n) }
map_err
がまだあることに注目してください。
なぜって、 try!
と From
の定義を思い出してください。
ここでの問題は io::Error
や num::ParseIntError
といったエラー型を、私たち独自の CliError
に変換できる From
の実装が無いことです。
もちろん、これは簡単に直せます!
CliError
を定義したわけですから、それに対して From
を実装できます:
use std::io; use std::num; impl From<io::Error> for CliError { fn from(err: io::Error) -> CliError { CliError::Io(err) } } impl From<num::ParseIntError> for CliError { fn from(err: num::ParseIntError) -> CliError { CliError::Parse(err) } }
これらの実装がしていることは、From
に対して、どうやって他のエラー型を元に CliError
を作るのか、教えてあげているだけです。
このケースでは、単に対応する値コンストラクタを呼ぶことで構築しています。
本当に 普通は これくらい簡単にできてしまいます。
これでようやく file_double
を書き直せます:
use std::fs::File; use std::io::Read; use std::path::Path; fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> { let mut file = try!(File::open(file_path)); let mut contents = String::new(); try!(file.read_to_string(&mut contents)); let n: i32 = try!(contents.trim().parse()); Ok(2 * n) }
ここでしたのは map_err
を取り除くことだけです。
それらは try!
マクロがエラーの値に対して From::from
を呼ぶので、もう不要になりました。
これで動くのは、起こりうる全てのエラー型に対して From
の実装を提供したからです。
もし file_double
関数を変更して、なにか他の操作、例えば、文字列を浮動小数点数に変換させたいと思ったら、エラー型のヴァリアントを追加するだけです:
use std::io; use std::num; enum CliError { Io(io::Error), ParseInt(num::ParseIntError), ParseFloat(num::ParseFloatError), }
そして、新しい From
実装を追加します:
use std::num; impl From<num::ParseFloatError> for CliError { fn from(err: num::ParseFloatError) -> CliError { CliError::ParseFloat(err) } }
これで完成です!
もし、あなたのライブラリがカスタマイズされたエラーを報告しなければならないなら、恐らく、独自のエラー型を定義するべきでしょう。
エラーの表現を表にさらすか(例: ErrorKind
) 、隠しておくか(例: ParseIntError
)は、あなたの自由です。
いずれかに関係なく、最低でも String
による表現を超えたエラー情報を提供することが、ほとんどの場合、良い方法となるしょう。
しかしこれは紛れもなく、ユースケースに深く依存します。
最低でも Error
トレイトを実装するべきでしょう。
これにより、ライブラリの利用者に エラーを合成する ための、最低ラインの柔軟性を与えます。
Error
トレイトを実装することは、利用者がエラーの文字列表現を取得できると保証することにもなります(なぜなら、こうすると fmt::Debug
と fmt::Display
の実装が必須になるからです)。
さらには、あなたのエラー型に対して From
の実装を提供するのも便利かもしれません。
このことは、(ライブラリ作者である)あなたと利用者が、 より詳細なエラーを合成する ことを可能にします。
例えば csv::Error
は io::Error
と byteorder::Error
の両方に From
実装を提供しています。
最後に、お好みで Result
型エイリアス を定義したくなるかもしれません。
特にライブラリでエラー型を一つだけ定義しているときは当てはまります。
この方法は標準ライブラリの io::Result
や fmt::Result
で用いられています。
このセクションは長かったですね。 あなたのバックグラウンドにもよりますが、内容が少し濃すぎたかもしれません。 たくさんのコード例に、散文的な説明が添えられる形で進行しましたが、これは主に学習を助けるために、あえてこう構成されていたのでした。 次はなにか新しいことをしましょう。ケーススタディです。
ここでは世界の人口データを問い合わせるための、コマンドラインプログラムを構築します。 目標はシンプルです:プログラムに場所を与えると、人口を教えてくれます。 シンプルにも関わらず、失敗しそうな所がたくさんあります!
ここで使うデータは データサイエンスツールキット から取得したものです。 これを元に演習で使うデータを準備しましたので、2つのファイルのどちらかをダウンロードしてください: 世界の人口データ (gzip圧縮時 41MB、解凍時 145MB)と、 アメリカ合衆国の人口データ (gzip 圧縮時 2.2MB、解凍時 7.2MB)があります。
いままで書いてきたコードでは、Rustの標準ライブラリだけを使うようにしてきました。
今回のような現実のタスクでは、最低でもCSVデータをパースする部分と、プログラムの引数をパースして、自動的にRustの型にデコードする部分に何か使いたいでしょう。
これには csv
と rustc-serialize
クレートを使います。
Cargoを使ってプロジェクトをセットアップしますが、その方法はすでに Hello, Cargo! と Cargoのドキュメント でカバーされていますので、ここでは簡単に説明します。
何もない状態から始めるには、cargo new --bin city-pop
を実行し、 Cargo.toml
を以下のように編集します:
[package]
name = "city-pop"
version = "0.1.0"
authors = ["Andrew Gallant <jamslam@gmail.com>"]
[[bin]]
name = "city-pop"
[dependencies]
csv = "0.*"
rustc-serialize = "0.*"
getopts = "0.*"
これでもう実行できるはずです:
cargo build --release
./target/release/city-pop
# 出力: Hello, world!
引数のパースができるようにしましょう。
Getoptsについては、あまり深く説明しませんが、詳細を解説した ドキュメント があります。
簡単に言うと、Getoptsはオプションのベクタから、引数のパーサーとヘルプメッセージを生成します(実際には、ベクタは構造体とメソッドの背後に隠れています)。
パースが終わると、プログラムの引数をRustの構造体へとデコードできます。
そこから、例えば、フラグが指定されたかとか、フラグの引数がなんであったかといった、フラグの情報を取り出せるようになります。
プログラムに適切な extern crate
文を追加して、Getoptsの基本的な引数を設定すると、こうなります:
extern crate getopts; extern crate rustc_serialize; use getopts::Options; use std::env; fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } fn main() { let args: Vec<String> = env::args().collect(); let program = &args[0]; let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_path = &args[1]; let city = &args[2]; // 情報を元にいろいろなことをする }
訳注
- Usage: {} [options]
:使用法:{} [options] - Show this usage message.:この使用法のメッセージを表示する。
このように、まず、このプログラムに渡された引数のベクタを取得します。
次に、最初の要素、つまり、プログラムの名前を格納します。
続いて引数フラグをセットアップしますが、今回はごく簡単なヘルプメッセージフラグが一つあるだけです。
セットアップできたら Options.parse
を使って引数のベクタをパースします(インデックス0はプログラム名ですので、インデックス1から始めます)。
もしパースに成功したら、パースしたオブジェクトをマッチで取り出しますし、失敗したならパニックさせます。
ここまでたどり着いたら、ヘルプフラグが指定されたか調べて、もしそうなら使用法のメッセージを表示します。
ヘルプメッセージのオプションはGetoptsにより生成済みですので、使用法のメッセージを表示するために追加で必要なのは、プログラム名とテンプレートだけです。
もしユーザーがヘルプフラグを指定しなかったなら、変数を用意して、対応する引数の値をセットします。
コードを書く順番は人それぞれですが、エラーハンドリングは最後に考えることが多いでしょう。
これはプログラム全体の設計にとっては、あまり良いことではありません。
しかし、ラピッドプロトタイピングには便利かもしれません。
Rustは私たちにエラーハンドリングが明示的であることを( unwrap
を呼ばせることで)強制しますので、プログラムのどこがエラーを起こすかは、簡単にわかります。
このケーススタディでは、ロジックは非常にシンプルです。
やることは、与えられたCSVデータをパースして、マッチした行にあるフィールドを表示するだけです。
やってみましょう。
(ファイルの先頭に extern crate csv;
を追加することを忘れずに。)
use std::fs::File; // この構造体はCSVファイルの各行のデータを表現します。 // 型に基づいたデコードにより、文字列を整数や浮動小数点数にパースして // しまうといった、核心部分のエラーハンドリングの大半から解放されます。 #[derive(Debug, RustcDecodable)] struct Row { country: String, city: String, accent_city: String, region: String, // 人口、経度、緯度などのデータは全ての行にあるわけではありません! // そこで、これらは不在の可能性を許す `Option` 型で表現します。 // CSVパーサーは、これらを正しい値で埋めてくれます。 population: Option<u64>, latitude: Option<f64>, longitude: Option<f64>, } fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } fn main() { let args: Vec<String> = env::args().collect(); let program = &args[0]; let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_path = &args[1]; let city: &str = &args[2]; let file = File::open(data_path).unwrap(); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = row.unwrap(); if row.city == city { println!("{}, {}: {:?}", row.city, row.country, row.population.expect("population count")); } } }
訳注:
population count:人口のカウント
ここで、エラーの概要を把握しましょう。
まずは明白なところ、つまり unwrap
が呼ばれている3ヶ所から始めます:
File::open
が io::Error
を返すかもしれない。csv::Reader::decode
は1度に1件のレコードをデコードし、 レコードのデコード (Iterator
の impl の Item
関連型を見てください)は csv::Error
を起こすかもしれない。row.population
が None
なら、 expect
の呼び出しはパニックする。他にもありますか?
もし一致する都市が見つからなかったら?
grep
のようなツールはエラーコードを返しますので、ここでも、そうするべきかもしれません。
つまり、IOエラーとCSVパースエラーの他に、このプログラム特有のエラーロジックがあるわけです。
これらのエラーを扱うために、2つのアプローチを試してみましょう。
まず Box<Error>
から始めたいと思います。
その後で、独自のエラー型を定義すると、どのように便利になるかを見てみましょう。
Box<Error>
によるエラー処理Box<Error>
の良いところは とにかく動く ことです。
エラー型を定義して From
を実装する、といったことは必要ありません。
これの欠点は Box<Error>
がトレイトオブジェクトなので 型消去 され、コンパイラが背後の型を推測できなくなることです。
以前 コードのリファクタリングを、関数の型を T
から Result<T, 私たちのエラー型>
に変更することから始めました。
ここでは 私たちのエラー型
は単に Box<Error>
です。
でも T
は何になるでしょう?
それに main
にリターン型を付けられるのでしょうか?
2つ目の質問の答えはノーです。できません。
つまり新しい関数を書くことになります。
では T
は何になるでしょう?
一番簡単にできるのは、マッチした Row
値のリストを Vec<Row>
として返すことです。
(もっと良いコードはイテレータを返すかもしれませんが、これは読者の皆さんへの練習問題とします。)
該当のコードを、専用の関数へとリファクタリングしましょう。
ただし unwrap
の呼び出しはそのままにします。
また、人口のカウントがない場合は、いまは単にその行を無視することに注意してください。
use std::path::Path; struct Row { // 変更なし } struct PopulationCount { city: String, country: String, // これは `Option` から変更します。なぜなら、この型の値は // 人口のカウントがあるときだけ構築されるようになったからです。 count: u64, } fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <data-path> <city>", program))); } fn search<P: AsRef<Path>>(file_path: P, city: &str) -> Vec<PopulationCount> { let mut found = vec![]; let file = File::open(file_path).unwrap(); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = row.unwrap(); match row.population { None => { } // スキップする Some(count) => if row.city == city { found.push(PopulationCount { city: row.city, country: row.country, count: count, }); }, } } found } fn main() { let args: Vec<String> = env::args().collect(); let program = &args[0]; let mut opts = Options::new(); opts.optflag("h", "help", "Show this usage message."); let matches = match opts.parse(&args[1..]) { Ok(m) => { m } Err(e) => { panic!(e.to_string()) } }; if matches.opt_present("h") { print_usage(&program, opts); return; } let data_path = &args[1]; let city = &args[2]; for pop in search(data_path, city) { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } }
expect
(unwrap
の少し良い版)の使用を1つ取り除くことができましたが、検索の結果が無いときのハンドリングは、依然として必要です。
このエラーを適切に処理するためには、以下のようにします:
search
のリターン型を Result<Vec<PopulationCount>, Box<Error>>
に変更する。try!
マクロ を使用することで、プログラムをパニックする代わりに、エラーを呼び出し元に返す。main
でエラーをハンドリングする。やってみましょう:
fn main() { use std::error::Error; // The rest of the code before this is unchanged // ここ以前の他のコードに変更なし fn search<P: AsRef<Path>> (file_path: P, city: &str) -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> { let mut found = vec![]; let file = try!(File::open(file_path)); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = try!(row); match row.population { // None => { } // skip it None => { } // スキップする Some(count) => if row.city == city { found.push(PopulationCount { city: row.city, country: row.country, count: count, }); }, } } if found.is_empty() { Err(From::from("No matching cities with a population were found.")) } else { Ok(found) } } }use std::error::Error; // ここ以前の他のコードに変更なし fn search<P: AsRef<Path>> (file_path: P, city: &str) -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> { let mut found = vec![]; let file = try!(File::open(file_path)); let mut rdr = csv::Reader::from_reader(file); for row in rdr.decode::<Row>() { let row = try!(row); match row.population { None => { } // スキップする Some(count) => if row.city == city { found.push(PopulationCount { city: row.city, country: row.country, count: count, }); }, } } if found.is_empty() { Err(From::from("No matching cities with a population were found.")) } else { Ok(found) } }
訳注:
No matching cities with a population were found.:
条件に合う人口データ付きの都市は見つかりませんでした。
x.unwrap()
の代わりに、今では try!(x)
があります。
私たちの関数が Result<T, E>
を返すので、エラーの発生時、 try!
マクロは関数の途中で戻ります。
このコードで1点注意があります:
Box<Error>
の代わりに Box<Error + Send + Sync>
を使いました。
こうすると、プレーンな文字列をエラー型に変換できます。
この From
実装 を使うために、このような追加の制限が必要でした。
// 上のコードでは `&'static str` に対して `From::from` を呼ぶことで、 // こちらの実装を使おうとしています。 impl<'a, 'b> From<&'b str> for Box<Error + Send + Sync + 'a> // もし `format!` などを使ってエラーメッセージのために新しい文字列を // 割り当てる場合は、こちらの実装も使えます。 impl From<String> for Box<Error + Send + Sync>
search
が Result<T, E>
を返すようになったため、 main
は search
を呼ぶときに場合分けをしなければなりません:
... match search(&data_file, &city) { Ok(pops) => { for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } Err(err) => println!("{}", err) } ...
Box<Error>
を使った適切なエラーハンドリングについて見ましたので、次は独自のエラー型による別のアプローチを試してみましょう。
でもその前に、少しの間、エラーハンドリングから離れて、 stdin
からの読み込みをサポートしましょう。
このプログラムでは、入力としてファイルをただ一つ受け取り、1回のパスでデータを処理しています。 これは、標準入力からの入力を受け付けたほうがいいことを意味しているのかもしれません。 でも、いまの方法も捨てがたいので、両方できるようにしましょう!
標準入力のサポートを追加するのは実に簡単です。 やることは3つだけです:
-f
オプションからファイルを得られるようにする。search
関数を修正して、ファイルパスを オプションで
受け取れるようにする。もし None
なら標準入力から読み込む。まず、使用法を変更します:
fn main() { fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <city>", program))); } }fn print_usage(program: &str, opts: Options) { println!("{}", opts.usage(&format!("Usage: {} [options] <city>", program))); }
次のパートはやや難しくなります:
fn main() { ... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); ... let file = matches.opt_str("f"); let data_file = &file.as_ref().map(Path::new); let city = if !matches.free.is_empty() { &matches.free[0] } else { print_usage(&program, opts); return; }; match search(data_file, city) { Ok(pops) => { for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } Err(err) => println!("{}", err) } ... }... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); ... let file = matches.opt_str("f"); let data_file = &file.as_ref().map(Path::new); let city = if !matches.free.is_empty() { &matches.free[0] } else { print_usage(&program, opts); return; }; match search(data_file, city) { Ok(pops) => { for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } } Err(err) => println!("{}", err) } ...
訳注:
Choose an input file, instead of using STDIN:
STDINを使う代わりに、入力ファイルを選択する。
このコードでは(Option<String>
型の) file
を受け取り、 search
が使える型、つまり今回は &Option<AsRef<Path>>
へ変換します。
そのためには file
の参照を得て、それに対して Path::new
をマップします。
このケースでは as_ref()
が Option<String>
を Option<&str>
へ変換しますので、続いて、そのオプション値の中身に対して Path::new
を実行することで、新しいオプション値を返します。
ここまでできれば、残りは単に city
引数を取得して search
を実行するだけです。
search
の修正は少し厄介です。
csv
トレイトは io::Read
を実装している型 からなら、いずれかを問わず、パーサーを構築できます。
しかし両方の型に同じコードが使えるのでしょうか?
これを実際する方法は2つあります。
ひとつの方法は search
を io::Read
を満たす型パラメータ R
に対するジェネリックとして書くことです。
もうひとつの方法は、以下のように、トレイトオブジェクトを使うことです:
use std::io; // ここ以前の他のコードに変更なし fn search<P: AsRef<Path>> (file_path: &Option<P>, city: &str) -> Result<Vec<PopulationCount>, Box<Error+Send+Sync>> { let mut found = vec![]; let input: Box<io::Read> = match *file_path { None => Box::new(io::stdin()), Some(ref file_path) => Box::new(try!(File::open(file_path))), }; let mut rdr = csv::Reader::from_reader(input); // これ以降は変更なし! }
以前、どうやって 独自のエラー型を使ってエラーを合成する のか学びました。
そのときはエラー型を enum
型として定義して、Error
と From
を実装することで実現しました。
3つの異なるエラー(IO、CSVのパース、検索結果なし)がありますので enum
として3つのヴァリアントを定義しましょう:
#[derive(Debug)] enum CliError { Io(io::Error), Csv(csv::Error), NotFound, }
Display
と Error
を実装します:
impl fmt::Display for CliError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { CliError::Io(ref err) => err.fmt(f), CliError::Csv(ref err) => err.fmt(f), CliError::NotFound => write!(f, "No matching cities with a \ population were found."), } } } impl Error for CliError { fn description(&self) -> &str { match *self { CliError::Io(ref err) => err.description(), CliError::Csv(ref err) => err.description(), CliError::NotFound => "not found", } } fn cause(&self) -> Option<&error::Error> { match *self { CliError::Io(ref err) => Some(err), CliError::Parse(ref err) => Some(err), // 今回の自前のエラーは下流の原因となるエラーは持っていませんが // そのように変更することも可能です。 CliError::NotFound() => None, } } }
CliError
を search
関数の型に使う前に、いくつかの From
実装を用意しなければなりません。
どのエラーについて用意したらいいのでしょう?
ええと io::Error
と csv::Error
の両方を CliError
に変換する必要があります。
外部エラーはこれだけですので、今は2つの From
実装だけが必要になるのです:
impl From<io::Error> for CliError { fn from(err: io::Error) -> CliError { CliError::Io(err) } } impl From<csv::Error> for CliError { fn from(err: csv::Error) -> CliError { CliError::Csv(err) } }
try!
がこのように定義 されているので、 From
の実装が重要になります。
特にエラーが起こると、エラーに対して From::from
が呼ばれますので、このケースでは、それらのエラーが私たち独自のエラー型 CliError
へ変換されます。
From
の実装ができましたので、search
関数に2つの小さな修正が必要です:
リターン型と「not found」エラーです。
全体はこうなります:
fn search<P: AsRef<Path>> (file_path: &Option<P>, city: &str) -> Result<Vec<PopulationCount>, CliError> { let mut found = vec![]; let input: Box<io::Read> = match *file_path { None => Box::new(io::stdin()), Some(ref file_path) => Box::new(try!(File::open(file_path))), }; let mut rdr = csv::Reader::from_reader(input); for row in rdr.decode::<Row>() { let row = try!(row); match row.population { None => { } // スキップする Some(count) => if row.city == city { found.push(PopulationCount { city: row.city, country: row.country, count: count, }); }, } } if found.is_empty() { Err(CliError::NotFound) } else { Ok(found) } }
これ以外の変更は不要です。
汎用的なコードを書くのは素晴らしいことです。 なぜなら、物事を汎用的にするのはクールですし、後になって役立つかもしれません。 でも時には、その苦労の甲斐がないこともあります。 最後のステップで何をしたか振り返ってみましょう:
Error
と Display
の実装を追加し、2つのエラーに対して From
も実装した。ここでの大きな問題は、このプログラムは全体で見ると大して良くならなかったことです。
enum
でエラーを表現するには、多くの付随する作業が必要です。
特にこのような短いプログラムでは、それが顕著に現れました。
ここでしたような独自のエラー型を使うのが便利といえる 一つの 要素は、 main
関数がエラーによってどう対処するのかを選択できるようになったことです。
以前の Box<Error>
では、メッセージを表示する以外、選択の余地はほとんどありませんでした。
いまでもそうですが、例えば、もし --quiet
フラグを追加したくなったらどうでしょうか?
--quiet
フラグは詳細な出力を抑止すべきです。
いま現在は、プログラムがマッチするものを見つけられなかったとき、それを告げるメッセージを表示します。 これは、特にプログラムをシェルスクリプトから使いたいときなどは、扱いにくいかもしれません。
フラグを追加してみましょう。 以前したように、使用法についての文字列を少し修正して、オプション変数にフラグを追加します。 そこまですれば、残りはGetoptsがやってくれます:
fn main() { ... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); opts.optflag("q", "quiet", "Silences errors and warnings."); ... }... let mut opts = Options::new(); opts.optopt("f", "file", "Choose an input file, instead of using STDIN.", "NAME"); opts.optflag("h", "help", "Show this usage message."); opts.optflag("q", "quiet", "Silences errors and warnings."); ...
後は「quiet」機能を実装するだけです。
main
関数の場合分けを少し修正します:
match search(&args.arg_data_path, &args.arg_city) { Err(CliError::NotFound) if args.flag_quiet => process::exit(1), Err(err) => panic!("{}", err), Ok(pops) => for pop in pops { println!("{}, {}: {:?}", pop.city, pop.country, pop.count); } }
訳注:
Silences errors and warnings.:エラーや警告を抑止します。
もちろん、IOエラーが起こったり、データのパースに失敗したときは、エラーを抑止したくはありません。
そこで場合分けを行い、エラータイプが NotFound
かつ --quiet
が指定されたかを検査しています。
もし検索に失敗したら、今まで通り( grep
の動作にならい)なにも表示せず、exitコードと共に終了します。
もし Box<Error>
で留まっていたら、 --quiet
機能を実装するのは、かなり面倒だったでしょう。
これが、このケーススタディの締めくくりとなります。 これからは外の世界に飛び出して、あなた自身のプログラムやライブラリを、適切なエラーハンドリングと共に書くことができるでしょう。
このセクションは長いので、Rustにおけるエラー処理について簡単にまとめたほうがいいでしょう。 そこには「大まかな法則」が存在しますが、これらは命令的なものでは断固として ありません 。 それぞれのヒューリスティックを破るだけの十分な理由もあり得ます!
unwrap
を使っても大丈夫かもしれません( Result::unwrap
, Option::unwrap
, Option::expect
のいずれかが使えます)。
あなたのコードを参考にする人は、正しいエラーハンドリングについて知っているべきです。(そうでなければ、このセクションを紹介してください!)unwrap
を使うことに罪悪感を持たなくてもいいでしょう。
ただし警告があります:もしそれが最終的に他の人たちの手に渡るなら、彼らが貧弱なエラーメッセージに動揺してもおかしくありません。String
か Box<Error + Send + Sync>
のいずれかを使ってください( Box<Error + Send + Sync>
は From
実装がある ので使えます)。From
と Error
を実装することで try!
マクロをエルゴノミックにしましょう。std::error::Error
トレイトを実装してください。
もし必要なら From
を実装することで、ライブラリ自身と呼び出し元のコードを書きやすくしてください。
(Rustの調和性規則(coherence rule) により、呼び出し側では、あなたのエラー型に対して From
を実装することはできません。
ライブラリでするべきです。)Option
と Result
で定義されているコンビネータについて学んでください。
それだけを使うのは大変ですが、 try!
と コンビネータを適度にミックスすることは、個人的には、とても魅力的な方法だと考えています。
and_then
, map
, unwrap_or
が私のお気に入りです。