第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を開催した。
前回話してた継承で例外を処理するのではなく、新たに例外を定義して処理をまとめていく方法を学んだ。とはいえエラー原因の取り扱いとかもう少し詳しく知りたい...。
今週のプルリクエストはこちら。