続・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は本来一緒には使えないようだ。
つまり…どういうことだってばよ?