起こり得ない失敗を素早く握りつぶしたいという欲求
通常のプログラミングだとエラーハンドリングをする必要性があるところを, 競プロの場合「入力がフォーマットにしたがってさえいれば絶対にエラーが起こらないのでハンドリングをサボりたい」という要求が頻繁にある。
Rust の場合, その多くはOption#unwrap()
およびResult#unwrap()
によって行われている。 これは成功していた場合値を取り出し, 失敗していた場合パニックする。
しかし, そのために 8 文字もタイプしなければいけないというのはちと面倒だ。他の言語だったら!
みたいな演算子が用意されているのだが。
これに限らず Rust の Null ハンドリングは少々手間がかかる。??
演算子はOption#unwrap_or()
だし, nullable?.hoge()
に至っては存在しないのでnullable.map(|x| x.hoge())
ないしnullable.map(Nullable::hoge)
とする。
代わりに, といってはなんだが?
演算子が存在する。 これは成功していた場合値を取り出し, 失敗していた場合はそれ自体を返しながらアーリーリターンする。 それ自体を返すので, 呼び出される関数自体もResult
でなければならない。 これは Java の検査例外のthrows
と同等の機能を型機能のみで実現しつつ, 例外機構とほぼ変わらない書き心地を提供してくれるかなりの発明だ。
int someFunction() throws SomeException {
String str = someFailableFunction(); /* コレ自体が throws SomeException */
/* ... */
return i;
}
fn some_function() -> Result<i32, SomeError> {
let str: String = some_failable_function()?; /* コレの返り値は Result<String, SomeError> */
/* ... */
Ok(i)
}
これを使えばハンドリングの省力化を実現できる。
詐欺じゃねえか
?
演算子はResult
に用意されていたtry!
マクロの演算子化である。 main
の返り値をResult<(), Box<dyn Error>>
とすると, たしかにResult
は投げられるがOption
は投げられない。
?
演算子のオーバーロードのために用意されようとしているTry
トレイトでは, into_result
メソッドによってResult
への変換ができることが要請されている。
しかし, それでもResult<(), Box<dyn Error>>
の中でOption
を投げることができない。 なぜかというと, Option#into_result
によって得られるエラー型であるNoneError
は, Error
トレイトを実装していないからである。 詐欺じゃん!!
この仕様については何度も議論がなされているようだ。 ここやここなど。 大抵の場合,
そんなに
Result
に変換したいなら, なんの情報も持たないNoneError
をそのまま使うよりも,Optional#ok_or
でその問題に関するより詳細な情報を持たせるほうがより良い解決策でしょ。
みたいなことを言われてしまっている。 いやまあたしかにそれはそうなんだけど俺の目的からしたらok_or("")?
とか書くのはunwrap()
って書いてるのと変わんねえって!
Haskell の型クラスと違い, Rust のトレイトは既存の型に既存のトレイトを実装することは許されていないので八方塞がりである。
独自のエラー型に変換しよう
Box<dyn Error>
で横着することを諦めれば, 各種エラーとNoneError
から変換できる独自のエラー型を定義して, それをmain
の返り値にすることで解決できなくはない。
もちろん, 発生しうるエラー型すべてに変換用のトレイトを実装しなければいけないのが面倒ではある。 しかし, そもそも正しいエラーハンドリングのあり方とはそうであるし, 競技中ではなく前もって用意できるテンプレートであるので許容範囲だろう。
ちなみに, 列挙せずにジェネリクスを使用して
impl From<NoneError> for MyError {
fn from(_: NoneError) -> Self {
MyError
}
}
impl<T: Error> From<T> for MyError {
fn from(_: T) -> Self {
MyError
}
}
みたいな横着した書き方はできない。なぜなら, たとえいまNoneError
が!Error
であっても将来的にError
を実装する可能性が否定できないため, このふたつの実装は衝突しているからである。 そんなとこに文句言う暇あったら実装してくれや。 トレイト境界を設けず<T>
にしろって? アーアー聞こえない。
とはいえさすがにボイラープレート極まりないのでマクロを定義することにして, せっかくなのでもとのエラー情報も使いつつ実装したのが以下の感じである。
struct CompeError {
source: Option<Box<dyn std::error::Error>>,
}
impl std::fmt::Debug for CompeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.source {
Some(x) => x.fmt(f),
None => writeln!(f, "NoneError"),
}
}
}
impl std::fmt::Display for CompeError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.source {
Some(x) => x.fmt(f),
None => writeln!(f, "attempted to retrieve a value does not exist"),
}
}
}
impl std::error::Error for CompeError{}
macro_rules! ce {
(std::option::NoneError) => {
impl From<std::option::NoneError> for CompeError {
fn from(_: std::option::NoneError) -> Self {
CompeError{source: None}
}
}
};
($t: ty) => {
impl From<$t> for CompeError {
fn from(e: $t) -> Self {
CompeError{source: Some(Box::new(e))}
}
}
};
}
ce!{std::option::NoneError}
ce!{core::num::ParseIntError}
/* あとはここに追記していく */
/* ... */
fn main() -> Result<(), CompeError> {
let sout = std::io::stdout();
let mut writer = std::io::BufWriter::new(sout.lock());
input! {
};
let line = format!("{}\n", ans);
writer.write(line.as_bytes())?;
Ok(())
}
おあとがよろしいようで
書いて動かせるのを確認したはいいものの, このコードは実際には競プロでは使用できない。 なぜならTry
トレイトそのものが Unstable なので Nightly でしか利用できないからだ(ドリフの笑い声)。 じゃあNoneError
が!Error
だろうがError
だろうが関係ないわな。
ま, スニペットにok_or("■━⊂( ・∀・) 彡ガッ☆ ( д ) ° °")
でも登録しておきましょ。