RustのResult/Option処理フロー

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は、文法が共通なコンビネータを持ち、メソッドチェーンで連続的に内部のTを加工できます。

  • 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"
               })?;
⁋ 2020/12/27↻ 2024/11/07
中馬崇尋
Chuma Takahiro