Rustには異常な状態を取り扱うための、Result<T,E>
型とOption<T>
型があり、関数型プログラミングで一般的な処理フローを実装できます。
この2つの型は、似たような構造になっていますが、以下の表のとおり、Result型の方がエラー時の表現力が詳細です。
type | left hand | right hand |
---|---|---|
Result | Ok(T) | Err(E) |
Option | Some(T) | None |
Result<T, E>とOption
map()
: Ok(T)/Some(T)の場合のみ実行。内部クロージャがT
を返すとmap()
がOk(T)/Some(T)にラップする。クロージャ内でErr()/Noneになり得る処理には使えないand_then()
: Ok(T)/Some(T)の場合のみ実行。and_then()
はクロージャの結果をラップしない。必要に応じてOk(T)/Some(T)にラップする必要があるが、Err()/Noneも返せる
メソッドチェーンの途中で Err(E)/None に遭遇するとその後のmap()/and_then()はスキップされるため、クロージャ内では Err(E)/None をチェックする処理が不要になります。
複数の変換を行うケースでは、途中でunwrap()
せずResult/Optionのまま処理した方が簡潔になります。
Result<T, E>とOptionが混在するチェーン
コンビネータは非常に便利ですが、map()やand_then()のメソッドチェーンの前後で、Result型とOption型が食い違うとエラーになります。
前後の型は、クロージャ内で利用する関数が返す型の影響を受けます。素朴にT型を返すような処理であれば単にmap()
で接続するだけで問題ありません。
しかし、以下のように利用するライブラリにより、ResultやOptionを返すものがあり注意が必要です。
- Nullableなメンバーを取得する関数はOptionを返すことが多い
- 通信エラーなどの起きうる値を取得する関数はResultを返すことが多い
- フォーマット変換する関数は、異常値が入力される場合などに備えてResultを返すことが多い
たとえば、以下のようにNullableな値を取得したうえでResultを返す関数 convert_result() を利用する場合、型を変換してResult型に統一したチェーンにすると動作します。
let result: Option<String> = nullable.get_option("data") // -> Option<T>
.ok_or("no data".to_string()) // -> Result<T, E>
.and_then(|data| data.convert_result()) // -> Result<T, E>
.map(|s| s.to_string()) // -> Result<T, E>
.ok(); // -> Option<T>
この例では、convert_result()のプロセスがResult型を受け取る必要があるという制約がポイントです。
型の変換には、以下のメソッドを利用します。
ok_or()
: Option -> Resultに変換。引数はエラーメッセージ。値がないことを示すエラーメッセージが適切ok()
: Result -> Option に変換。None
は固定値であるため引数なし、エラーの詳細は失われる
途中でErr(E)
に遭遇した場合には、後続処理をスキップして最終的にok()
によりNoneに変換されます。ok_or()
でエラーメッセージを指定しましたが、この例ではok()
に直行し単に捨てられます。
Resultではなくok()
を用いて途中過程をOption
?演算子による早期リターン
メソッドチェーンの羅列を避けたい場合、文末の?
演算子による早期リターンを活用できます。
?演算子は、Ok(T)であればunwrap()し、Err(E)に遭遇すると関数からreturnします。
先ほどの類似例では、以下のようなコードになります。
fn convert(nullabale: SomeNullable) -> Result<String, String> {
let data = nullable.get_option("data")
.ok_or("no data".to_string())?;
let value = data.convert_result()?;
// すべてOk(T)のケースだけ到達する
Ok(value.to_string())
}
途中過程でErrorのケースが排除されているため、最終的にOk()
にラップして返す構造になります。
map_err()によるエラー変換
?
演算子を利用するとその場で関数からリターンするため、エラーの型を関数のシグネチャに合わせたいケースが出てきます。
map_err()
関数を使うと、エラーの場合に、エラーをクロージャ内で自由に変更できます。また、変更前のエラーもログなどに利用できます。
let data = nullable.get_option("data")
.map_err(|e| {
debug!("{:?}, e");
"fetch data error"
})?;
Chuma Takahiro