Hatena::ブログ(Diary)

cooldaemonの備忘録 RSSフィード

2011-10-17

Either と scalaz.Validation

下記で取り上げられているネタについてメモを残す。

Either と Scalaz

Either は flatMap メソッドを持たないので for 式では使えない。for 式内で Right で処理を進めたいなら right メソッドで、Left で処理を進めたいなら left メソッドで *Projection を取得する必要がある。

def r(n: Int): Either[String, Int] = Right(n)
for {
  x <- r(1).right;
  y <- r(x).right
} yield x+y

とはいえ、多くの場合 Right で処理を進めたい場合が多く、Left で処理を進めたい事は稀である。心情的には、下記のように書きたい。

def r(n: Int): Either[String, Int] = Right(n)
for {x <- r(1); y <- r(x)} yield x+y

そこで scalaz の出番となる。下記は、問題なく動作する。

import scalaz._
import Scalaz._
def r(n: Int): Either[String, Int] = Right(n)
for {
  x <- r(1);
  y <- r(x)
} yield x+y

以降のサンプルコードは、下記のように scalaz が import 済みである事を必須とする。

import scalaz._
import Scalaz._

scalaz を使っているので、下記のようにも書ける。

for {
  x <- 1.right[String];
  y <- x.right[String]
} yield x+y

これを、flatMap と map を使った形式に書き換えると下記の通り。

1.right[String] flatMap {x => x.right[String] map {y => x + y}}

scalaz では下記のエイリアスも使える。

1.right[String] >>= {x => x.right[String] &#8728; {y => x + y}}

&#8728; は ∘ です

scalaz.Validation

実は、for 式の中で使う分には、Either と変わらない。

(for {x <- 1.success[String]; y <- x.success[String]} yield x+y) assert_=== 2.success[String]
(for {x <- "foo".fail[Int];   y <- x.success[String]} yield x+y) assert_=== "foo".fail[Int]

scalaz.Validation の flatMap と map のソースコードは、現在の所、下記の通りとなっているので、上記は当たり前。

//..snip..
  def map[B](f: A => B): Validation[E, B] = this match {
    case Success(a) => Success(f(a))
    case Failure(e) => Failure(e)
  }
//..snip..
  def flatMap[EE >: E, B](f: A => Validation[EE, B]): Validation[EE, B] = this match {
    case Success(a) => f(a)
    case Failure(e) => Failure(e)
  }
//..snip..

では、Either と Validation の違いは何なのか?下記のようなコードを記述した際に違いが現れる。

(1.success[String] <*> (1.success[String] map ((_: Int) + (_: Int)).curried)) assert_=== 2.success[String]
("foo".fail[Int] <*> (1.success[String] <*> ("bar".fail[Int] map ((_: Int) + (_: Int) + (_: Int)).curried))) assert_=== "barfoo".fail[Int]

全てが Success の際には map で繋いだ関数が評価され、Failure の場合は内包されている値が積み重なっている。bar が前にあるが、評価順は左から右。

Validation は、Monad であるならば Applicative でもあるので Validation[A => B] を引数に取る <*> メソッドを持つ。

実際に Failure を積み重ねている箇所は Apply.scala 内の下記。

  implicit def ValidationApply[X: Semigroup]: Apply[({type λ[α]=Validation[X, α]})#λ] = new Apply[({type λ[α]=Validation[X, α]})#λ] {
    def apply[A, B](f: Validation[X, A => B], a: Validation[X, A]) = (f, a) match {
      case (Success(f), Success(a)) => success(f(a))
      case (Success(_), Failure(e)) => failure(e)
      case (Failure(e), Success(_)) => failure(e)
      case (Failure(e1), Failure(e2)) => failure(e1 &#8889; e2)
    }
  }

&#8889; は ⊹ です

Failure側(左側)の X を半群(Semigroup)で束縛した新しい型コンストラクタを型引数として与えた Apply を new している。半群なので、当然、二項演算 ⊹(|+| でも良い) を持っている。

ちなみに「({type λ[α]=Foo[X, α]})#λ」は、X を束縛した新しい無名型コンストラクタを作る定型文として覚えると scalaz のコードを読みやすくなる。

幾つか、書き換えの例を示す。

("foo".fail[Int].<***>(1.success[String], "bar".fail[Int]) {_ + _ + _}) assert_=== "foobar".fail[Int]
(("foo".fail[Int] |@| 1.success[String] |@| "bar".fail[Int]) {_ + _ + _}) assert_=== "foobar".fail[Int]
(("foo".fail[Int] &#8859; 1.success[String] &#8859; "bar".fail[Int]) {_ + _ + _}) assert_=== "foobar".fail[Int]

&#8859; は ⊛ です

上記の場合、評価順は <*> 同様に左から右。結果は <*> と逆順。こちらの方が見た目も結果も解りやすい。何故、こような結果になるかは… MA.scala の定義を参照の事(手抜き)。

このようなコードの書き方(Applicative スタイルと言う)は、下記の注意点がある。

  • 連結した式全てが評価される
  • 式の評価結果を次の式へ渡せない

これらの事から Validation は、正に Validation 処理に適していると言える(w;

蛇足だが、Either の Apply は、下記の通り。

//..snip..
trait Applys {
  def FunctorBindApply[Z[_]](implicit t: Functor[Z], b: Bind[Z]) = new Apply[Z] {
    def apply[A, B](f: Z[A => B], a: Z[A]): Z[B] = {
      lazy val fv = f
      lazy val fa = a
      b.bind(fv, (g: A => B) => t.fmap(fa, g(_: A)))
    }
  }
}
//..snip..
object Apply extends ApplyLow {
//..snip..
  implicit def EitherApply[X]: Apply[({type λ[α]=Either[X, α]})#λ] = FunctorBindApply[({type λ[α]=Either[X, α]})#λ]
//..snip..
}

Either の fmap は下記の通り。

implicit def EitherFunctor[X]: Functor[({type λ[α]=Either[X, α]})#λ] = new Functor[({type λ[α]=Either[X, α]})#λ] {
  def fmap[A, B](r: Either[X, A], f: A => B) = r match {
    case Left(a) => Left(a)
    case Right(a) => Right(f(a))
  }
}

Either の bind は、下記の通り。

implicit def EitherBind[X]: Bind[({type λ[α]=Either[X, α]})#λ] = new Bind[({type λ[α]=Either[X, α]})#λ] {
  def bind[A, B](r: Either[X, A], f: A => Either[X, B]) = r.fold(Left(_), f)
}

って事で(何が?)下記の通りとなる(手抜き)。

("foo".left[Int] <*> (1.right[String] <*> ("bar".left[Int] map ((_: Int) + (_: Int) + (_: Int)).curried))) assert_=== "bar".left[Int]
(("foo".left[Int] |@| 1.right[String] |@| "bar".left[Int]) {_ + _ + _}) assert_=== "foo".left[Int]

for 式で使った場合と異なり、左から右へ全ての式が評価された後に、結果がまとめられる。

記号について

scalaz ではメソッド名に記号が用いられているが、はてダ に投稿する際に &#8728; のように変換されてしまう事がある。これ、どうにかならないかな?

どうやらコードを貼付けた箇所のみ、そうなる様子

参考資料

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

トラックバック - http://d.hatena.ne.jp/cooldaemon/20111017/1318862426