広島大学ITエンジニアアドベントカレンダー Advent Calendar 2019の2日目の記事です。
広島大学工学部4年生荒木勇登です!
来年から福岡でITエンジニアします!
いみこというハンドルネームでTwitterをやっていますのでそちらもご確認ください。
(https://twitter.com/es__135 )
Rustの難所といえば所有権と型システムだと思いますが、エラーハンドリングもなかなかハマりポイントだと思います。そのエラーハンドリングを自分も勉強中の身ですが行列の足し算を例題としながら解説したいと思います。
#普通に書くとどうなるか
type Matrix = Vec<Vec<i32>>;
fn mat_print(a: &Matrix) {
for i in a {
println!("{:?}",i);
}
println!("行は{}です",a.len());
println!("列は{}です",a[0].len());
}
fn add(x: &Matrix, y: &Matrix) -> Matrix{
let row = x[0].len();
let col = x.len();
let mut z = vec![vec![0;row];col];
for i in 0..row {
for j in 0..col {
z[i][j] = x[i][j] + y[i][j];
}
}
z
}
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
mat_print(&add(&_a,&_b));
}
ちらっとコードの解説をします。
一行目typeエイリアスを用いて行列の型Matrixを定義しています。
(参考:https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/type-aliases.html)
add関数で行列の足し算をしています。
println!("{:?}",Matrix);
で行列を出力すると横長に出力されて見にくいので行列の形で出力される関数mat_printを用意します。
それでmain関数で行列を定義して、計算結果を出力します。出力結果を見てみましょう。
[2, 5, 8]
[11, 14, 7]
[10, 13, 16]
行は3です
列は3です
はい、計算できました。おしまい!!
じゃ記事にならないので、この関数にすこし意地悪をしましょう。
エラーメッセージをはかせるようにする。
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
mat_print(&add(&_a,&_b));
}
行列_aの行を短くしました。これでは行列の足し算が成り立ちません。
これを実行するとどうなるでしょうか。
thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 2', /rustc/625451e376bb2e5283fc4741caa0a3e8a2ca4d54/src/libcore/slice/mod.rs:2715:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
当然エラーが出ます。エラーが出るのは当然なのですが、問題はエラーの内容です。
このエラーメッセージは範囲外のインデックスを見てますよーという意味なのですが、ちょっとわかりにくいです。できたら行列のサイズが合ってないですよーってエラーメッセージが出た方が親切だと思いませんか?
もうちょっとわかりやすいエラーメッセージを返すようにadd関数をいじっていきましょう。
fn add(x: &Matrix, y: &Matrix) -> Matrix{
let row = x[0].len();
let col = x.len();
let cal_can = (x.len() == y.len() && x[0].len() == y[0].len()); //xとyのサイズ>が同じか計算する
if !cal_can {panic!("行列のサイズが合わないので計算できません。")};
let mut z = vec![vec![0;row];col];
for i in 0..row {
for j in 0..col {
z[i][j] = x[i][j] + y[i][j];
}
}
z
}
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
mat_print(&add(&_a,&_b));
}
このように書くとエラーメッセージが下記のようになります。
panic!に関する詳しい説明はこちら(https://doc.rust-jp.rs/book/second-edition/ch09-01-unrecoverable-errors-with-panic.html)
thread 'main' panicked at '行列のサイズが合わないので計算できません。', src/main.rs:7:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
自分で設定したようにエラーを吐かせることができました。
いまはシンプルなプログラムを作っているのでだからどうした?って感じかもしれませんが、これが複雑なプログラムを書く必要が出てくるとデバックするときに大変助かるようになります。
#途中で止まらないようにしたい
ちょっと設定をいじってみます。
fn add(x: &Matrix, y: &Matrix) -> Matrix{
let row = x[0].len();
let col = x.len();
let cal_can = (x.len() == y.len() && x[0].len() == y[0].len());
if !cal_can {panic!("行列のサイズが合わないので計算できません。")};
let mut z = vec![vec![0;row];col];
for i in 0..row {
for j in 0..col {
z[i][j] = x[i][j] + y[i][j];
}
}
z
}
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
let _c: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
mat_print(&add(&_a,&_b));
mat_print(&add(&_b,&_c));
}
aとbは計算できませんが、bとcは計算できます。
これを実行するとどうなるでしょうか。
thread 'main' panicked at '行列のサイズが合わないので計算できません。', src/main.rs:15:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
当然といえば当然ですが、上と同じエラーがでます。
このままでも良いのですが、できたらaとbは計算不可能というエラーメッセージを出す、bとcは計算結果を出力してほしいですよね。そのような機能を実装するためにRustにはResultという機能があります。
(参考:Resultに関する詳しい説明はこちら
https://doc.rust-jp.rs/book/second-edition/ch09-02-recoverable-errors-with-result.html)
fn add_cal(x: &Matrix, y: &Matrix) -> Matrix {
let row = x[0].len();
let col = x.len();
let mut z: Matrix = vec![vec![0;row];col];
for i in 0..row {
for j in 0..col {
z[i][j] = x[i][j] + y[i][j];
}
}
z
}
fn add(x: &Matrix, y: &Matrix) -> Result<Matrix,String>{
let cal_can = x.len() == y.len() && x[0].len() == y[0].len();
let z:Result<Matrix,String> = match cal_can {
true => Ok(add_cal(&x,&y)),
false => Err("行列のサイズが合わないので計算できません。".to_string()),
};
z
}
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
let _c: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
match add(&_a,&_b) {
Ok(mat) => mat_print(&mat),
Err(err) => println!("{}:{}",std::any::type_name(err),err),
};
match add(&_b,&_c) {
Ok(Mat) => mat_print(&Mat),
Err(err) => println!("エラー{}",err),
};
}
と書きますと出力がこうなります。
エラー行列のサイズが合わないので計算できません。
[2, 5, 8]
[11, 14, 7]
[10, 13, 16]
行は3です
列は3です
とこのように書くとエラーで途中で処理が止まらず、最後までプログラムを計算してくれます。
#String返すよりエラー型返したくない?
現在エラーの型としてStringを返していますがこれはちょっと気持ち悪いので、独自にエラー型を設定します。
type SizeErr = String;
type Matrix = Vec<Vec<i32>>;
fn mat_print(a: &Matrix) {
for i in a {
println!("{:?}",i);
}
println!("行は{}です",a.len());
println!("列は{}です",a[0].len());
}
fn add_cal(x: &Matrix, y: &Matrix) -> Matrix {
let row = x[0].len();
let col = x.len();
let mut z: Matrix = vec![vec![0;row];col];
for i in 0..row {
for j in 0..col {
z[i][j] = x[i][j] + y[i][j];
}
}
z
}
fn add(x: &Matrix, y: &Matrix) -> Result<Matrix,SizeErr>{
let is_same_col = x.len() == y.len();
let is_same_row = x[0].len() == y[0].len();
let err_meth:SizeErr = match (is_same_col,is_same_row) {
(true,true) => "".to_string(),
(true,false) => "列の大きさが異なり計算不可能です".to_string(),
(false,true) => "行の大きさが異なり計算不可能です".to_string(),
(false,false) => "行も列も大きさが異なり計算不可能です".to_string(),
};
let z:Result<Matrix,String> = match is_same_col && is_same_row {
true => Ok(add_cal(&x,&y)),
false => Err(err_meth),
};
z
}
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
let _c: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
match add(&_a,&_b) {
Ok(mat) => mat_print(&mat),
Err(err) => println!("エラー{}",err),
};
match add(&_b,&_c) {
Ok(Mat) => mat_print(&Mat),
Err(err) => println!("エラー{}",err),
};
}
エラー行の大きさが異なり計算不可能です
[2, 5, 8]
[11, 14, 7]
[10, 13, 16]
行は3です
列は3です
エラーメッセージもすこし工夫しておきました(Stringのままでもできましたが)
#unwrap()
ここまで読まれた方の一部には、えっResultっていちいちmatch文書かなきゃいけないの??めんどくさ????とお思った方に朗報です。
unwrap()って書くだけでResultは外れて正しいデータが入ってればそのデータを取得できます。ただし、計算が間違っていた場合エラーとなりそのプログラムはその時点でストップしその後は計算されません。
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
let _c: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
mat_print(&add(&_a,&_b).unwrap());
mat_print(&add(&_b,&_c).unwrap());
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "行の大きさが異なり計算不可能です"', src/libcore/result.rs:1084:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
こんな感じにエラーでプログラムが止まってしまいます。
上のプログラムの下二行の上下を入れ替えると、
fn main() {
let _a: Matrix = vec![vec![1,2,3],
vec![7,8,9]];
let _b: Matrix = vec![vec![1,3,5],
vec![7,9,1],
vec![3,5,7]];
let _c: Matrix = vec![vec![1,2,3],
vec![4,5,6],
vec![7,8,9]];
mat_print(&add(&_b,&_c).unwrap());
mat_print(&add(&_a,&_b).unwrap());
}
[2, 5, 8]
[11, 14, 7]
[10, 13, 16]
行は3です
列は3です
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "行の大きさが異なり計算不可能です"', src/libcore/result.rs:1084:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
というふうにunwrap()を使うとmatchで条件分岐しなくても情報を取得できます。が、unwrap()を使うとResultを使っている意味があまりなくなるので用法用量に気をつけて使いましょう。
#参考文献
Resultで回復可能なエラー
https://doc.rust-jp.rs/book/second-edition/ch09-02-recoverable-errors-with-result.html
プログラミング言語Rust エラーハンドリング
https://doc.rust-jp.rs/the-rust-programming-language-ja/1.6/book/error-handling.html