続・ScalazのValidationでFizzBuzz

前回のヤツのいくつか補足。
まだまだ勉強中だけど。

ValidationでFizzBuzz(anonymous left版)

scalaz/example/ExampleApplicative.scala見てたらFizzBuzzにぴったりのオペレータ見つけたので書き直した。

import scalaz._
import Scalaz._

object FizzBuzzByValidation {
    def makeValidator(d:Int, msg:String) = {
        (n:Int) => if (n % d != 0)
                n.success
            else
                msg.fail
    }
    val fizz = makeValidator(3, "Fizz")
    val buzz = makeValidator(5, "Buzz")
    def fizzBuzz(n:Int) {
        fizz(n) <* buzz(n) match {
            case Success(p) => println(p)
            case Failure(e) => println(e)
        }
    }
    def main(args: Array[String]) {
        for (n <- 1 to 100)
            fizzBuzz(n)
    }
}

「<*」ってどんな演算子か?サンプルを見てみる。

    def s[A](a: A) = a.success[List[String]]
    def f[A](s: String) = ff(List(s))
    def ff[A](s: List[String]) = s.fail[Int]

    s(7) <* s(8) assert_=== s(7)
    s(7) <* f("bzzt") assert_=== f("bzzt")
    f("bang") <* s(8) assert_=== f("bang")
    f("bang") <* f("bzzt") assert_=== ff(List("bang", "bzzt"))

「<*」はSuccess同士なら左側を、どちらか一方がFailureならそれを、両方がFailureなら中身をappendしたFailureを返してくれるらしい。
(ちなみに「*>」は右側を使ってくれる。今回はどっちでも良い。)
scalaz/MA.scalaにある「<*」の定義を見てみる。

  def <*[B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[A] = <**>(b)((a, b) => a)

よく分からんが((a, b) => a)という関数を<**>に渡している。
真似して書き換えるとこういう感じ。

    def fizzBuzz(n:Int) {
        (fizz(n) <**> buzz(n)) { (a, b) => a } match {
            case Success(p) => println(p)
            case Failure(e) => println(e)
        }
    }

前回の「|@|」を使った例と同じ形だ。但し|@|と違っていくつも連結するような使い方は出来ないっぽい。


内容をもうちょっと見ていく。
scalaz/MA.scalaにある「<**>」の定義を見てみる。

  def <**>[B, C](b: M[B])(z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] = a(t.fmap(value, z.curried), b)

t:Functor[M]とa:Apply[M]を適当に見つけてきてくれてzをcurriedして適応してくれるらしい。
valueはこのMAに包まれてるValidationそのもの(と思う。)
Validation用のFunctorとApplyはそれぞれのところに定義されている。
scalaz/Functor.scala:

  implicit def ValidationFunctor[X]: Functor[({type λ[α]=Validation[X, α]})#λ] = new Functor[({type λ[α]=Validation[X, α]})#λ] {
    def fmap[A, B](r: Validation[X, A], f: A => B) = r map f
  }

scalaz/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 |+| e2)
    }
  }

Validation用のmapはscalaz/Validation.scalaに定義されている

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

「{ (a, b) => a }.curried」は「{ (a) => { (b) => a } }」だったはず。
全部使って置き換えると、

    def vmap[E, A, B](value:Validation[E, A], f: A => B): Validation[E, B] = value match {
        case Success(a) => Success(f(a))
        case Failure(e) => Failure(e)
    }
    def vapply[E: Semigroup, A, B](f: Validation[E, A => B], a: Validation[E, 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 |+| e2)
    }
    def fizzBuzz(n:Int) {
        vapply(vmap(fizz(n), { (a:Int) => { (b:Int) => a } }), buzz(n)) match {
            case Success(p) => println(p)
            case Failure(e) => println(e)
        }
    }

まあ分かったような分からないような……


あと、scalaz/MA.scalaには「<*>」というオペレータがあって

  def <*>[B](f: M[A => B])(implicit a: Apply[M]): M[B] = a(f, value)

これと「<**>」の定義と「<**>」を使った例を見比べると、「<**>」を使った例は「<*>」を使って次のように書ける。

    def fizzBuzz(n:Int) {
        buzz(n) <*> (fizz(n) map { (a:Int, b:Int) => a }.curried) match {
            case Success(p) => println(n)
            case Failure(e) => println(e)
        }
    }

「map」は「∘」と同じ意味です。
これがたぶんいわゆるApplicative スタイルに一番近い形?
HaskellのApplicative スタイルは「func <$> arg1 <*> arg2 <*> ...」らしい*1ので、見た目の順序が逆になっている。

WriterでFizzBuzz(Applicative版)

WriterにもFunctorやApplyへの変換が定義されているので、Validationと同じようにApplicativeに書けるようだ。

import scalaz._
import Scalaz._

object FizzBuzzByApplicativeWriter {
    implicit def WriterMAB[W, A](w: Writer[W, A]): MAB[Writer, W, A] = mab[Writer, W, A](w)

    def makeOps(d:Int, msg:String) = {
        (n:Int) =>
            if (n % d != 0)
                n.pure[PartialApply1Of2[Writer, String]#Apply]
            else
                n.set(msg)
    }
    val fizz = makeOps(3, "Fizz")
    val buzz = makeOps(5, "Buzz")
    def fizzBuzz(n:Int) {
        (fizz(n) |@| buzz(n)) { (a, b) => a } value match {
            case (s, over) if s == mzero[String] => println(over)
            case (written, _)                    => println(written)
        }
    }

    def main(args: Array[String]) {
        for (n <- 1 to 100)
            fizzBuzz(n)
    }
}

「<*>」と「∘」(map)を使っても書ける。

    def fizzBuzz(n:Int) {
        buzz(n) <*> (fizz(n) map { (a:Int, b:Int) => a }.curried) value match {
            case (s, over) if s == mzero[String] => println(over)
            case (written, _)                    => println(written)
        }
    }


逆にValidationの方をMonadicスタイル(?)で書くとどうか。

import scalaz._
import Scalaz._

object NoFizzBuzzByValidation {
    import Validation.Monad._

    def makeValidator(d:Int, msg:String) = {
        (n:Int) =>
            if (n % d != 0)
                n.success
            else
                msg.fail
    }
    val fizz = makeValidator(3, "Fizz")
    val buzz = makeValidator(5, "Buzz")
    def fizzBuzz(n:Int) {
        n.success >>= fizz >>= buzz value match {
            case Success(p) => println(p)
            case Failure(e) => println(e)
        }
    }
    def main(args: Array[String]) {
        for (n <- 1 to 100)
            fizzBuzz(n)
    }
}

これは動くけどFizzBuzzにならない。

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
Fizz   // not FizzBuzz!
16
17
...

Validationは処理を>>=で結合して行った場合、最初に失敗したところで処理を打ち切ってしまうので、3でも5でも割り切れる場合でも"Fizz"しか出力されない。


同一入力欄で文字種エラー→数値範囲エラーのように前段が成功した場合だけ後段を実行したいときはflatMapで、入力欄Aと入力欄Bのように両方チェックして両方をエラー文言を足しこみたい時は|@|で繋ぐという風に使える。一応。

import scalaz._
import Scalaz._

object Person {
  case class Person(age: Int, name: String, postcode: String)

  // 数値チェック
  def validInteger(s: String): Validation[List[String], Int] =
    try {
      Success(s.toInt)
    } catch {
      case e => Failure(List(e.toString))
    }
  // 年齢の入力チェック
  def validAge(a: Int): Validation[List[String], Int] =
    if(a < 0)
      Failure(List("Age must be greater than 0"))
    else if(a > 130)
      Failure(List("Age must be less than 130"))
    else
      Success(a)

  // 名前の入力チェック
  def validName(s: String): Validation[List[String], String] =
    if(s.headOption exists (_.isUpper))
      Success(s)
    else
      Failure(List("Name must begin with a capital letter"))

  // 郵便番号の入力チェック
  def validPostcode(s: String): Validation[List[String], String] =
    if(s.length == 4 && s.forall(_.isDigit))
      Success(s)
    else
      Failure(List("Postcode must be 4 digits"))

  // 人物情報の入力チェック
  def validPerson(age:String, name:String, postcode:String): Validation[List[String], Person] = {
     (
       (validInteger(age)
               flatMap (validAge _))
       |@| validName(name)
       |@| validPostcode(postcode)
     ) (Person)
  }
  def checkPerson(age:String, name:String, postcode:String) = {
     validPerson(age, name, postcode) match {
      case Success(p) => println("We have a person: " + p)
      case Failure(e:List[String]) => {
        println("Some errors occured: ")
        e foreach ((a) => println("  x. " + a))
      }
    }
  }

  def main(args: Array[String]) {
    checkPerson("30", "Fred", "4000")
    checkPerson("-7", "red", "490000")
    checkPerson("boo", "Fred", "490000")
    checkPerson("30", "", "411x")
  }
}

こうやって小さいValidationを組み合わせて大きいValidationを作れると。


但しソースコードのコメントを見ると、

object Validation {
  import Scalaz._
  
  /**
   * This instance is inconsistent with the Applicative instance for Validation -- errors are *not*
   * accumulated. Consider using Either or Either.RightProjection instead.
   *
   * If you want to us this, explicitly `import Validation.Monad._`
   */
  object Monad {
...

ということでそもそもValidationのApplicativeバージョンの(エラーがaccmulateされる)|@|とMonadバージョンのflatMapは本来一緒には使えないようだ。


つまり…どういうことだってばよ?