2012年2月16日木曜日

Scala Tips / Either (4) - getOrElse, flatMap

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

Eitherから値を取り出す処理として以下の2つのコーディングパターンを挙げました。

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

前回は、後者のEither[A, B]からCへの変換について、基本的なイディオムを紹介しました。

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

今回は、Either[A, B]がLeft[A, B]の場合だけでなく、Right[A, B]の値が条件を満たさない場合も、デフォルト値を返すようにします。

条件結果演算
Either[A, B]がRight[A, B]で有効な値が入っているCBからCを計算
Either[A, B]がRight[A, B]で無効な値が入っているCデフォルト値
Either[A, B]がLeft[A, B]Cデフォルト値

以下では、Either[Exception, Int]からStringへの変換を例に考えてみます。ただし、Intは0以上のものが有効という条件を追加します。Right[Int]に入っているIntが0以上の場合、Stringが処理結果となります。一方、0未満の場合は無効となり空文字列「""」が処理結果となります。またEither[A, B]がLeft[A, B]の場合も空文字列「""」が処理結果となります。

(分類の基準)

Java風

if式でEither#isRightを使って右側の値の有無を判定します。デフォルト値の「""」はelse句で指定します。isRightメソッドが一回、getメソッドが二回、泣き別れになってしまいます。

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

Scala風

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

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

Scala

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

前回は計算文脈の切り替えは行わなかったのでmapメソッドを使いましたが、今回は計算文脈の切り替えを行うのでflatMapメソッドを使います。

以下ではEitherからrightメソッドでRightProjectionを取り出しflatMapメソッド内で、Right[Exception, Int]の値が0未満である場合はLeft(new IllegalArgumentException("bad"))を返すことで失敗の計算文脈に切り替える処理を行っています。

def f(a: Either[Exception, Int]): String = {
  a.right.flatMap { b =>
    if (b >= 0) Right(b.toString)
    else Left(new IllegalArgumentException("bad"))
  }.right getOrElse ""
}
Optionに変換

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

前述の例では、new IllegalArgumentException("bad")で生成したExceptionを次の処理ですぐに捨てています。もったいないですね。

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

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

Scalaz

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

def f(a: Either[Exception, Int]): String = {
  a.flatMap { b =>
    if (b >= 0) b.toString.right
    else new IllegalArgumentException("bad").left
  }.right getOrElse ""
}

また、Booleanのeitherメソッドを使う方法もあります。

def f(a: Either[Exception, Int]): String = {
  a.flatMap { b =>
    !(b >= 0) either {
      new IllegalArgumentException("bad")
    } or b.toString
  }.right getOrElse ""
}

SclazではflatMapを「>>=」の記号で記述することができます。上の例はそれぞれ以下のようになります。

def f(a: Either[Exception, Int]): String = {
  (a >>= { b =>
    if (b >= 0) b.toString.right
    else new IllegalArgumentException("bad").left
  }).right getOrElse ""
}
def f(a: Either[Exception, Int]): String = {
  (a >>= { b =>
    !(b >= 0) either {
      new IllegalArgumentException("bad")
    } or b.toString
  }).right getOrElse ""
}
Optionに変換

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

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

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

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

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

ノート

前回のmapメソッドと同様に、flatMapメソッドを使う場合も、計算文脈をEitherからOptionに切り替えるテクニックが有力です。

Eitherを使う場合は、計算文脈を成功文脈から失敗文脈に切り替える時にflatMapメソッドを使うしか選択肢はありませんが、Optionにすることで、withFilterメソッドやcollectメソッドなども使えるようになります。

また、ScalazはOptionに対して色々な機能拡張をしているので、さらにプログラミングの選択肢が広がります。今回の例では「|」メソッドやorZeroメソッドで値を取り出しています。

Monadicプログラミングでは、計算文脈の中で文脈を切り替えていく(Eitherの中でLeftとRightを切り替え)のに加え、計算文脈毎、応用に適したものに切り替えていく(EitherをOptionに切り替え)のも重要なテクニックになります。

モナドで実現された階層化複合化された計算文脈を適材適所で切り替えながら処理を進めていくのがMonadicプログラミングの醍醐味と言えるかもしれません。

flatMapと「>>=」と「∗」

Scalazは型クラスをベースにしたMonadicプログラミングを指向したクラスライブラリですが、Scala言語本体にもモナドが実装されています。このため、モナドの基本操作の部分ではScala本体とScalazの機能がバッティングします。

ボク個人のプログラミング方針としては、Scala本体とScalazがバッティングしていて機能がほとんど変わらない場合はScala本体を選ぶようにしています。というのは、Scalazは余分なオブジェクトを相当数生成しながら動作するのでかなり重たいからです。

ただ、これは簡潔に記述できることとのトレードオフでもありますし、Scalazを選んだ以上多少のことは目をつぶって簡潔さに賭けるというのが潔く、現在のハードウェア性能では多くの場合許容範囲と考えられます。

さらに本記事のようなScalazイディオムという意味では、よりScalazらしい書き方を主にした方が趣旨に合います。

そういうこともあって、ボクの書き癖でflatMapを使ってきましたが、Scalazらしく機能がバッティングする場合も「>>=」といったScalaz側のメソッドを主にしていくことにしました。

また、flatMapは「∗」、mapは「∘」という記号(Unicode)で記述する方法もあり、flatMap、「>>=」、「∗」のどれを選ぶのか悩ましいところです。「∗」は「∘」と紛らわしいので、flatMap、「>>=」、「∗」の選択では「>>=」を使うことにします。(Haskellの流儀に則っていて、モナドや型クラス周りの知識のある人には読みやすいというのもあります。)

なお、Eitherに関していうと、Scala標準ではEitherにはflatMapメソッドはないので、Scalazを使っていることを明示する目的で「>>=」を使うという考え方もあります。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿