2012年2月15日水曜日

Scala Tips / Either (3) - getOrElse

Eitherから値を取り出すイディオムです。

EitherをOption的な成功/失敗文脈で使う方法について考えています。右側が成功文脈、左側が失敗文脈としています。

Eitherの場合も、Optionと同様に値を取り出す処理として以下の2つのコーディングパターンがあります。

  • Either[A, B]からEither[A, C]に変換
  • Eigher[A, B]からCに変換

ここまではEither[A, B]からEither[A, C]への変換についてみてきました。今回はEither[A, B]からCへの変換について考えます。

条件結果演算
Either[A, B]がRight[A, B]CBからCを計算
Either[A, B]がLeft[A, B]Cデフォルト値

Either[A, B]がRight[A, B]の場合は、この値からCを計算しますが、Left[A, B]の場合はあらかじめ用意しているデフォルト値を返します。

以下では、Either[Exception, Int]からStringへの変換を例に考えてみます。Either[Exception, Int]がLeft[Exception]の場合はデフォルト値として空文字列「""」を返すことにします。

(分類の基準)

Java風

if式でEither#isRightを使って右側の値の有無を判定します。デフォルト値の「""」はelse句で指定します。isRightメソッドとgetメソッドが泣き別れになっているのがあまりよい感触ではありません。

def f(a: Either[Exception, Int]): String = {
  if (a.isRight) a.right.get.toString
  else ""
}

Scala風

match式を使うと以下のようになります。こちらの方が綺麗です。

def f(a: Either[Exception, Int]): String = {
  a match {
    case Right(b) => b.toString
    case Left(_) => ""
  }
}

Scala

EitherでMonadic演算をしたい場合にはright(またはleft)メソッドでRightProjection(またはLeftProjection)を取得し、これに対してmapメソッドやflatMapメソッドを適用します。EitherとRightProjection/LeftProjectionの組合せでモナド的な動作をします。

以下ではEitherからrightメソッドでRightProjectionを取り出しmap適用、mapの結果のEitherからrightメソッドでRightProjectionを取り出しgetOrElseで値を取り出す、という形でEither→right→RightProjectionの連鎖で処理を進めています。

最後に、flatMapの結果のEitherからrightメソッドでRightProjectionを取り出しgetOrElseで値を取り出します。

def f(a: Either[Exception, Int]): String = {
  a.right.map(_.toString).right getOrElse ""
}
Optionに変換

最終的に値を取り出す場合、左側に保持しているエラー情報は捨てることになります。この場合には、エラー情報(Exception)を最初の段階で捨ててしまい、エラーか否かという情報のみを伝搬しても得られる結果は同じです。

そこで、最初の段階でRightProjectionのtoOptionメソッドでOptionに変換し、Optionに対してMonadic演算を行うようにします。

以下ではOptionに変換後、mapメソッドとgetOrElseメソッドの合わせ技で値を取得しています。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) getOrElse ""
}

Scalaz

ScalazではEitherが右側を成功文脈とする成功/失敗の文脈としても動作するので、Either#rightメソッドでRightProjectionを取得しなくてもEitherに対して直接mapメソッドを適用することができます。ただし、ScalazでもEitherにgetOrElseメソッドはないので、rightメソッドでRightProjectionを取得する必要があります。このためScala版と比べてそれほど違いは出てきません。

def f(a: Either[Exception, Int]): String = {
  a.map(_.toString).right getOrElse ""
}
Optionに変換

ScalazでもEitherにtoOptionメソッドは追加されないのでrightメソッドで得られるRightProjectionのtoOptionメソッドを用いてOptionに変換します。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) | ""
}

Scalazでは、Optionに対して色々な機能拡張を行っているので、Optionに変換すればいろいろな技を使うことが可能になります。ここでは「|」メソッドで値を取り出しています。

また、以下のようにデフォルト値がモノイドの単位元と同じ場合にはorZeroメソッドを使うこともできます。

def f(a: Either[Exception, Int]): String = {
  a.right.toOption.map(_.toString) orZero
}

ノート

Eitherは、Optionに比べるとかなり扱いにくいオブジェクトです。Scala標準クラスライブラリでは、あくまでも直和、選択を表現するためのオブジェクトであり、典型的な使い方である成功/失敗文脈を扱うのに便利な機能(右側を特別扱いする関数など)が用意されていません。またEitherそのものはOptionのようなモナドでもありませんし、今の所利用頻度がそれほど多くないためか、その他Eitherを取り回す時に便利なユーティリティ機能が少ないので、Optionのような使いやすさにはなりません。

Scalazでも、多少状況は緩和されますが扱いにくいオブジェクトであることは変わりません。

Eitherの文脈を維持する必要がある場合は仕方ありませんが、今回のようにエラー情報そのものは捨ててもよい場合には、最初の段階で計算文脈をEither(エラー情報を保持する成功/失敗文脈)からOption(エラー情報を保持しない成功/失敗文脈)に切り替えるのが有力なテクニックです。

計算文脈の複雑度が低くなるのでプログラミングが扱うべき事項も小さくなります。また、OptionはScalaでもScalazでも、Eitherに比べると機能が豊富でいろいろな技が使えるので、プログラミングの選択肢が広がります。

エラー情報を保持しつつ、成功/失敗の計算文脈でプログラミングを行う場合は、ScalazのValidationが有力な選択肢になります。ValidationはEitherに続けて取り上げる予定です。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿