第61回です。前回はこちら。
[第61回の様子]
2022/12/14に第61回を開催した。
内容としてはRust By Example 日本語版の18. エラーハンドリングの「18.4.5. エラーをラップする」〜「18.5. Resultをイテレートする」に取り組んだ。18.5はちらっと見ただけだけど...。
参加者は自分を入れて5人。師走で忙しいけどたくさん集まってよかった。
[学んだこと]
- 18.4.5. エラーをラップする
- 前回はBox型を使ってエラーをまとめる方法を学んだ
- しかし、実行時まで型が分からないという問題があったので、今回は具体的なエラーをラップした型を定義していく
- 18.4.2. エラー型を定義するではOptionもParseIntErrorも同じエラー(struct)に変換していたが、ここではそれらを区別する
- まず、enumを用いてそれぞれのエラーを定義する
use std::num::ParseIntError; type Result<T> = std::result::Result<T, DoubleError>; #[derive(Debug)] enum DoubleError { // ベクターが空のとき EmptyVec, // ベクターの最初の要素がi32に変換できないとき Parse(ParseIntError), }
- 次に、DoubleErrorをエラーとして利用できるように、DisplayトレイトとErrorトレイトを実装していく
use std::error; use std::fmt; impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { DoubleError::EmptyVec => write!(f, "please use a vector with at least one element"), DoubleError::Parse(..) => write!(f, "the provided string could not be parsed as int"), } } } impl error::Error for DoubleError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { DoubleError::EmptyVec => None, DoubleError::Parse(ref e) => Some(e), } } }
- テキストでは
fmt()
メソッドのコメントに、ラップした元の型の実装に任せると書かれていたが、実際にはラップされた型を利用していない - 利用するとしたらこんな感じになりそう
impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { // 省略 // ref eで元のエラーを参照 DoubleError::Parse(ref e) => write!(f, "the provided string could not be parsed as int: original error={:?}", e), } } }
- さらに、
?
演算子でParseIntErrorをDoubleErrorに変換するために、Fromトレイトを実装する
use std::num::ParseIntError; impl From<ParseIntError> for DoubleError { fn from(err: ParseIntError) -> DoubleError { DoubleError::Parse(err) } }
- これを利用して、ベクターの最初の要素を2倍する関数を実装するとこうなる
// Errorトレイトのsource()メソッドを利用するために必要 use std::error::Error as _; fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or(DoubleError::EmptyVec)?; // ここで先ほどのFromトレイトの実装を呼び出す let parsed = first.parse::<i32>()?; Ok(2 * parsed) } fn print(result: Result<i32>) { match result { Ok(n) => println!("The first doubled is {}", n), Err(e) => { // Displayトレイトの実装を利用 println!("Error: {}", e); // Errorトレイトの実装を利用 if let Some(source) = e.source() { println!(" Caused by: {}", source); } }, } } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; print(double_first(numbers)); // The first doubled is 84 print(double_first(empty)); // Error: please use a vector with at least one element print(double_first(strings)); // Error: the provided string could not be parsed as int // Caused by: invalid digit found in string }
- sここまでやってみた結果、最後のまとめに書かれていたのが「ボイラープレートが増えて冗長になるのでライブラリを使いましょう、だったのがウケる
- anyhowとかを使いましょうねってことみたい
- 参加したメンバーのひとりが、dojoの後に例外の詳細を持たせる場合の実装を書いてくれた
- 最終的に
with_message()
がanyhowのcontext()
みたいになった
use std::error; use std::num::ParseIntError; use std::fmt; type Result<T> = std::result::Result<T, DoubleError>; #[derive(Debug)] enum DoubleError { EmptyVec(Option<String>), Parse(Option<String>, ParseIntError), } impl DoubleError { // メッセージ付きでEmptyVecを作成するコンストラクタ fn new(message: String) -> DoubleError { DoubleError::EmptyVec(Some(message)) } } // 自作のエラー型にメッセージを書き込むユーティリティ fn add_error_message(e: DoubleError, message: String) -> DoubleError { match e { DoubleError::EmptyVec(_) => DoubleError::EmptyVec(Some(message)), DoubleError::Parse(_, e) => DoubleError::Parse(Some(message), e), } } impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { DoubleError::EmptyVec(ref message) => write!(f, "DoubleError::EmptyVec: {}", show_optional_message(message)), DoubleError::Parse(ref message, ref wrapped_error) => write!(f, "DoubleError::Parse: {}\n Caused by: {}", // causeがあればそれも再帰的に表示する show_optional_message(message), wrapped_error), } } } // .or_empty_string() みたいなやつ fn show_optional_message(message: &Option<String>) -> &str { message.as_ref().map_or("", |s| s.as_str()) } impl error::Error for DoubleError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { DoubleError::EmptyVec(_) => None, DoubleError::Parse(_, ref e) => Some(e), } } } // `ParseIntError`から`DoubleError`への変換の実装。 // `ParseIntError`が`DoubleError`に変換される必要がある時、自動的に`?`から呼び出される。 impl From<ParseIntError> for DoubleError { fn from(err: ParseIntError) -> DoubleError { DoubleError::Parse(None, err) // デフォルトではエラーメッセージはなし } } // エイリアス型にメソッドを生やすためのトレイト trait MessageAppendable<T> { fn with_message<F: FnOnce() -> String>(self, message: F) -> Result<T>; } // こういう使い方がしたい: // result.withMessage(|| !format("Some error. param1={}, param2={}, ...", ...))?; // ? 演算子のfromに頼れなくなるので、よりジェネリックResultに対してwith_messageを定義する impl<T, U: Into<DoubleError>> MessageAppendable<T> for std::result::Result<T, U> { fn with_message<F: FnOnce() -> String>(self, message: F) -> Result<T> { self.map_err(|e| add_error_message(e.into(), message())) } } fn double_first(vec: Vec<&str>) -> Result<i32> { let first = vec.first().ok_or_else(|| DoubleError::new("please use a vector with at least one element".to_string()))?; let parsed = first.parse::<i32>() .with_message(|| format!("the provided string could not be parsed as int. value={}", &first))?; Ok(2 * parsed) }
- 18.5. Resultをイテレートする
- mapの結果がResultになる場合、collect()すると以下のようになる
fn main() { let strings = vec!["tofu", "93", "18"]; // ここはVec<Result<i32, std::num::ParseIntError>>と同じ let numbers: Vec<_> = strings .into_iter() .map(|s| s.parse::<i32>()) .collect(); println!("Results: {:?}", numbers); } // 出力は以下のようにErrかOkが返ってくる Results: [Err(ParseIntError { kind: InvalidDigit }), Ok(93), Ok(18)]
- これでは扱いづらいので、Okになった結果だけを集める場合には、filter_map()を利用する
fn main() { let strings = vec!["tofu", "93", "18"]; let numbers: Vec<i32> = strings .into_iter() .filter_map(|s| s.parse::<i32>().ok()) .collect(); println!("Results: {:?}", numbers); } // 出力は以下の通り Results: [93, 18]
Result.ok()
がOption<T>を返すので、それでNoneをフィルタしてくれるらしい。便利。
[まとめ]
モブプログラミングスタイルでRust dojoを開催した。
最近RustでLeetCodeやってるおかげもあって、最初の頃よりはスムーズに読み書きができるようになってきたと思う。
今週はあまり元のコードを変更しなかったのでプルリクエストなし。