第59回です。前回はこちら。
[第59回の様子]
2022/11/30に第59回を開催した。
内容としてはRust By Example 日本語版の18. エラーハンドリングの「18.3.3. 早期リターン」〜「18.4.2. エラー型を定義する」に取り組んだ。
参加者は自分を入れて6人くらい。勤労感謝の日で1週あいたけどみんな来てくれてよかった。
[学んだこと]
- 18.3.3. 早期リターン
- 前回はmap()やand_then()を用いてエラーハンドリングを行なった
- ここではmatchと早期リターンの組み合わせを見ていく
parse()
が返すResultをmatchで処理すると以下のようになる
use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = match first_number_str.parse::<i32>() { Ok(first_number) => first_number, Err(e) => return Err(e), }; let second_number = match second_number_str.parse::<i32>() { Ok(second_number) => second_number, Err(e) => return Err(e), }; Ok(first_number * second_number) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2")); // n is 20 print(multiply("t", "2")); // Error: invalid digit found in string }
- ちょっと冗長だし、first_number_strとsecond_number_strのどちらがエラーになったのか分かりづらいのでリターンする内容はもう少し工夫できそうな気もする
- 18.3.4. ?の導入
- Errを取り出すのに先ほどmatchを利用したが、
?
を利用するとより簡潔に書ける
use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = first_number_str.parse::<i32>()?; let second_number = second_number_str.parse::<i32>()?; Ok(first_number * second_number) } fn print(result: Result<i32, ParseIntError>) { match result { Ok(n) => println!("n is {}", n), Err(e) => println!("Error: {}", e), } } fn main() { print(multiply("10", "2"));// n is 20 print(multiply("t", "2"));// Error: invalid digit found in string }
?
が実装されるまでは同様の動作をtry!マクロで実装していた:古いコードで見られる
use std::num::ParseIntError; fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> { let first_number = try!(first_number_str.parse::<i32>()); let second_number = try!(second_number_str.parse::<i32>()); Ok(first_number * second_number) }
- try!マクロを利用する際はCargoのパッケージバージョンを2015に変更する必要があるらしい
- 最新版は2021なのでなかなか古い。
- 18.4. 複数のエラー型
- OptionとResultを組み合わせたり、異なるErrのResultを組み合わせる方法を学んでいく
- 例題として、配列の最初の要素を2倍して返す以下の関数を扱う
fn double_first(vec: Vec<&str>) -> i32 { let first = vec.first().unwrap(); // `Option::unwrap()` on a `None` value'の可能性 2 * first.parse::<i32>().unwrap() // ParseIntErrorの可能性 } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {}", double_first(numbers)); println!("The first doubled is {}", double_first(empty)); println!("The first doubled is {}", double_first(strings)); }
- 18.4.1. OptionからResultを取り出す
- 無理やりResultをOptionに埋め込むことで対処するとこうなる
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> { vec.first().map(|first| { first.parse::<i32>().map(|n| 2 * n) }) } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {:?}", double_first(numbers)); // Some(Ok(84)) println!("The first doubled is {:?}", double_first(empty)); // None println!("The first doubled is {:?}", double_first(strings)); // Some(Err(ParseIntError { kind: InvalidDigit })) }
- 反対に、OptionをResultに埋め込むこともできる
use std::num::ParseIntError; fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> { let opt = vec.first().map(|first| { first.parse::<i32>().map(|n| 2 * n) }); opt.map_or(Ok(None), |r| r.map(Some)) } fn main() { let numbers = vec!["42", "93", "18"]; let empty = vec![]; let strings = vec!["tofu", "93", "18"]; println!("The first doubled is {:?}", double_first(numbers)); // Ok(Some(84)) println!("The first doubled is {:?}", double_first(empty)); // Ok(None) println!("The first doubled is {:?}", double_first(strings)); // Err(ParseIntError { kind: InvalidDigit }) }
|r| r.map(Some)
の部分が少し分かりにくいが、rはResult<i32, ParseIntError>
型で、エラーがない場合は値をSomeで包んで返す- 上の例では
Ok(84)
が渡されてOk(Some(84))
に変換されている(Err(ParseIntErr)の場合はmapされないのでそのまま) - 18.4.2. エラー型を定義する
18.4.1
では、NoneとParseIntErrorを別々に扱う必要があった- これをまとめるために自前のエラー型(DoubleError)を定義していく
use std::error; use std::fmt; #[derive(Debug, Clone)] struct DoubleError; // エラー情報はシンプルなメッセージのみ impl fmt::Display for DoubleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "invalid first item to double") } } // Errorとして扱うためのトレイトを実装する impl error::Error for DoubleError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { // エラー原因は記録しない None } } // DoubleErrorをもつエイリアスを定義 type Result<T> = std::result::Result<T, DoubleError>;
- これを利用して配列の最初の要素を2倍にして返す関数を実装すると次のようになる
fn double_first(vec: Vec<&str>) -> Result<i32> { vec.first() // Noneの場合、自前のエラーに変換 .ok_or(DoubleError) .and_then(|s| { s.parse::<i32>() // Parseエラーを自前のエラーに変換 .map_err(|_| DoubleError) .map(|i| 2 * i) }) }
- ちょっと長くて読みづらいね、と話していたら、一度変数で受けるとand_then()を消せていいかも、という案がでた。
- ネストも浅くて済むし、良さそう。
fn double_first(vec: Vec<&str>) -> Result<i32> { let s = vec.first() // Noneの場合、自前のエラーに変換 .ok_or(DoubleError)?; s.parse::<i32>() // Parseエラーを自前のエラーに変換 .map_err(|_| DoubleError) .map(|i| 2 * i) }
[まとめ]
モブプログラミングスタイルでRust dojoを開催した。
前回話してた継承で例外を処理するのではなく、新たに例外を定義して処理をまとめていく方法を学んだ。とはいえエラー原因の取り扱いとかもう少し詳しく知りたい...。
今週のプルリクエストはこちら。