2012年2月14日火曜日

Scala Tips / Either (2) - flatMap

Eitherを成功/失敗の文脈で使用する方法のイディオムです。

前回はEitherをOptionと同じ成功/失敗の文脈で使用する方法について見てきました。

これは以下の演算になりますが、ちょうど、Optionでいうと「 Option (3) - map 」に相当する処理です。

条件結果
Either[A, B]がRight[A, B]Right[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

今回は以下の処理、アプリケーションの意図で成功の文脈を失敗の文脈に切り替えるためのMonadic演算についてみていきます。Optionでいうと「Option (6) - flatMap 」に相当する処理になります。

条件結果
Either[A, B]のRight[A, B]に有効な値が入っているRight[A, C]
Either[A, B]のRight[A, B]に無効な値が入っているLeft[A, C]
Either[A, B]がLeft[A, B]Left[A, C]

大枠ではB→Cの演算を行いたいわけですが、これをEither[A, B→C]の文脈の上で行います。この時、Either[A, B]がRight[A, B]であっても「B」が無効な値である場合には、Left[A, C]にすることで、成功の文脈から失敗の文脈へ切り替えます。

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

Java風

if式でEither#isRightメソッドを使ってLeftとRightの判定をして、処理を切り分ける事ができます。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  if (a.isRight) {
    if (a.right.get >= 0) {
      Right(a.right.get.toString)
    } else {
      Left(new IllegalArgumentException("less than 0"))
    }
  } else {
    a.asInstanceOf[Either[Exception, String]]
  }
}

Scala風

match式を使うとLeftとRightのパターンマッチングで綺麗に書くことができます。ただし、Scala的にはLeft(b)の場合はLeft(b)というロジックを書くのが悔しい。

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

Scala

Eitherのrightメソッドで得られるRightProjectionがモナドっぽい動きをするので、flatMapメソッドを使ってMonadicプログラミングします。flatMapメソッドでは、Int値が0以上である場合は有効な値なのでStringに変換する演算を行い結果をRightオブジェクトに詰めて返します。一方、Int値が0未満の場合は、無効な値なので成功の文脈から失敗の文脈への切り替えとして、LeftオブジェクトにIllegalArgumentExceptionを詰めて返します。

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

Scalaz

Scalazを使うと、Eitherが右側を成功の文脈として動作するモナドに拡張されるので、これを利用したプログラミングが可能です。やはりflatMapメソッドを使います。また、LeftとRightの生成をleftメソッド、rightメソッドで簡潔に記述できるようになります。

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

ノート

前回は、Eitherのrightメソッドで返ってくるRightProjectionを使えばMonadic演算が可能なことを説明しました。

RightProjectionを使って、Optionで使用したwithFilter, collect, flatMap, for式といった技が駆使できると嬉しいのですが、残念ながらそうはなっていません。以下の理由により、この中で使える/使って便利なのはflatMapのみとなります。

collectメソッドはRightProjectionにそもそも機能がありません。

withFilterメソッドやfilterメソッドはEither[A, B]ではなくOption[Either[A, B]]を返すので、Optionのハンドリングが余分に必要になります。これは、それなりのコードになってしまうので、それよりflatMapメソッドを使ったほうが簡潔です。

for式は内部的にwithFilterメソッドを使っているので、やはり使えません。

以上の理由で、Eitherに対しては他の機能のことは考えずに、文脈の切り替えはflatMapメソッド一本で考えていくのが得策です。

Scalazの場合も事情は同じで(Left/RightProjectionではなく)EitherのflatMapメソッドを使って処理を記述することになります。

追記 (2012-02-14)

ひなたねこさんのツイートでよりScalazらしい書き方が判明したので補足します。

以下はEither[Exception, Int]の生成にBooleanのeitherメソッドを使ったバージョンです。Right(…)、….rightやLeft(…)、….leftが出てこないので、より簡潔でScalazらしいですね。

def f(a: Either[Exception, Int]): Either[Exception, String] = {
  a.flatMap { b =>
    !(b >= 0) either {
      new IllegalArgumentException("less than 0")
    } or b.toString
  }
}
またScala版の記述をleftメソッド、rightメソッドを使うものに更新しました。

諸元

  • Scala 2.9.1
  • Scalaz 6.0.3

0 件のコメント:

コメントを投稿