第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やってるおかげもあって、最初の頃よりはスムーズに読み書きができるようになってきたと思う。
今週はあまり元のコードを変更しなかったのでプルリクエストなし。