Result
型
Nullableな値を扱う際にOption
型があるというのは前記事で取り上げましたが、より具体的に「処理の成否」を格納するための型としてResult
型というものがあります。
Result
型は内部にOk<T>
とErr<E>
(T
とE
はどちらもジェネリクス)の2つの要素を持ちます。処理が成功したときの値はOk<T>
に、失敗したときのエラーはErr<E>
に格納されます。
処理が失敗する可能性がある処理はResult
型を返すのが一般的です。Result
はOption
同様にmatch
によるパターンマッチングが利用できます。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10, 2);
match result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
let result = divide(10, 0);
match result {
Ok(value) => println!("Result: {}", value),
Err(error) => println!("Error: {}", error),
}
}
Ok
に値が入っているときにのみ特定の処理を行いたい場合、and_then
メソッドが使えます。match
を使うと、Err
の場合のパターンも記述しなくてはならない上に、分岐が連続するとブロックがネストされて見にくくなります。このメソッドを使うことで、記述量を減らせます。
fn parse_to_number(input: &str) -> Result<i32, String> {
input.parse::<i32>().map_err(|_| "Failed to parse number".to_string())
}
fn divide_by_two(num: i32) -> Result<i32, String> {
if num % 2 == 0 {
Ok(num / 2)
} else {
Err("Cannot divide an odd number by 2".to_string())
}
}
fn main() {
let result = Ok("42")
.and_then(parse_to_number) // "42" をパースして 42 を返す
.and_then(divide_by_two); // 42 を 2 で割り、21 を返す
match result {
Ok(value) => println!("Final result: {}", value),
Err(error) => println!("Error: {}", error),
}
}
また、Ok
に入った値をもとに新たな値を返したい場合はmap
メソッドが使えます。
fn main() {
let result = Ok(10)
.map(|x| x * 2) // 成功値 10 を 20 に加工
.map(|x| x + 5); // 20 に 5 を足して 25 に加工
match result {
Ok(value) => println!("Processed value: {}", value), // Processed value: 25
Err(error) => println!("Error: {}", error),
}
}
例外のthrowではなくResult
型を返却する、というのは(筆者のにわか知識ですが)関数型プログラミングを想像させます。このタイプのプログラミングはしたことがないので非常に興味があります。エラーハンドリングを型システムで定義するというのは安全そうですね。TypeScriptにもneverthrow
などのライブラリがあるので、いつか触ってみたいです。
and_then
もmap
も
-
Ok
が存在するときの処理を書ける - 値を返すことができる
という共通点があります。どう使い分けるのかはいろいろアイデアがありそうですが、個人的にはこちらの記事が非常に参考になりました。
筆者の理解では、
- 複数の処理をチェーンしたい、途中で失敗する可能性がある場合→
and_then
- 単純に値の変換だけしたい、失敗ケースは考えない場合→
map
と使い分けるのが良さそうに思えました。
Result
型に名前をつける
Result
は一種の型なので、よく使うであろうResult
型をtype
キーワードで宣言しておくと便利です。
type MyResult<T> = Result<T, String>;
fn divide(a: i32, b: i32) -> MyResult<i32> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn main() {
let result: MyResult<i32> = divide(10, 2);
println!("{:?}", result);
}
Early Return
いくつかの処理を連続して実行し、それぞれの処理の成否によっては処理を中断しErr
を返すパターンをEarly Return(早期リターン)と呼びます。
match
式が値を返せることを利用し、Ok
であればその値を変数にバインディングして次に進み、Err
であればErr
をreturn
するパターンです。
この方法であればネストは発生せず、かつ「処理Aが失敗したら全体的に失敗とするが、処理Bが失敗したらフォールバック値を使う」といった柔軟な処理が実装できます(Result
というより、Option
の特徴に着目しているといえます)。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn increment_if_even(x: i32) -> Result<i32, String> {
if x % 2 == 0 {
Ok(x + 1)
} else {
Err("Not an even number".to_string())
}
}
fn process(value: i32) -> Result<i32, String> {
let value = match divide(value, 2) {
Ok(v) => v,
Err(e) => return Err(e),
};
let value = match increment_if_even(value) {
Ok(v) => v,
Err(e) => return Err(e),
};
Ok(value)
}
fn main() {
match process(8) {
Ok(result) => println!("Success: {}", result),
Err(error) => println!("Error: {}", error),
}
}
?
を使って安全にOk
の値を取り出す
「ある処理の値がErr
であればそれをreturn
する」という前提のもと、より簡潔に書けるのが?
構文です。Result
型を返すメソッドの後に?
をつけると、Err
が返されたときのreturn
が不要になります(自動で行ってくれる)。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
fn add_five(x: i32) -> Result<i32, String> {
Ok(x + 5)
}
fn calculate(a: i32, b: i32) -> Result<i32, String> {
let result = divide(a, b)?; // Okなら値を取得、Errならreturn
add_five(result)
}
fn main() {
match calculate(10, 2) {
Ok(value) => println!("Final result: {}", value),
Err(error) => println!("Error: {}", error),
}
}