Scalaz で Writer モナド
今回は、ログを追記する Writer モナドを Scalaz で使ってみます。
- Scalaz 7.0.0-M3
- sbt 0.12.0
サンプルソースは http://github.com/fits/try_samples/tree/master/blog/20120917/
使用した sbt 用ビルドファイルは以下です。
build.sbt
scalaVersion := "2.10.0-M7" libraryDependencies += "org.scalaz" % "scalaz-core" % "7.0.0-M3" cross CrossVersion.full
単純な Writer モナド
まずは、単純な Writer モナドのサンプルです。
WriterT.writer に (ログ, 値) のタプルを渡して Writer モナドを生成します。
Writer モナドを返す関数(下記の logNum や logNumList)を >>= で繋げばログが追記されていきます。*1
下記サンプルではログ部分に String と List をそれぞれ使った Writer モナドを用意し、String 版では " + <値>" という文字列が、List 版では値そのものがログとして残るようにしています。
なお、ログの内容を取得するには written、値を取得するには value を使います。
WriterSample.scala
package fits.sample import scalaz._ import Scalaz._ import WriterT._ object WriterSample extends App { // ログに String を使った Write モナド生成( Writer[String, Int] ) val logNum = (n: Int) => (x: Int) => writer (s" + $n", n + x) val r1 = writer ("2", 2) >>= logNum(5) >>= logNum(3) println(s"${r1.written} = ${r1.value}") // >>= の代わりに for を使用 val r2 = for { w1 <- writer ("2", 2) w2 <- logNum(5)(w1) w3 <- logNum(3)(w2) } yield w3 println(s"${r2.written} = ${r2.value}") // ログに List を使った Writer モナド生成( Writer[List[Int], Int] ) val logNumList = (n: Int) => (x: Int) => writer (List(n), n + x) val r3 = writer (List(2), 2) >>= logNumList(5) >>= logNumList(3) println(s"${r3.written} = ${r3.value}") }
実行結果は以下の通り。
加算した値がログとして残っている事を確認できます。
実行結果
> sbt console ・・・ scala> fits.sample.WriterSample.main(null) 2 + 5 + 3 = 10 2 + 5 + 3 = 10 List(2, 5, 3) = 10
List モナドの機能を備えた Writer モナド (WriterT.writerT() の利用)
次に、List モナドの機能を備えた Writer モナドを使ってみます。
基本的に、WriterT.writerT() にモナドで包んだ (ログ, 値) のタプルを渡す事で、別のモナドの機能を備えた Writer モナドを取得できます。
下記サンプルでは、List モナドに包んだ (String, Int) を WriterT.writerT() に渡す事で WriterT[List, String, Int] *2 を取得しています。
ちなみに、Scalaz における Writer[A, B] 型は WriterT[scalaz.Id.Id, A, B] の別名です。
WriterTSample.scala
・・・ object WriterTSample extends App { // Writer モナド生成( WriterT[List, String, Int] ) val logManyNum = (n: Int) => (x: Int) => { val list = for { i <- (1 to n).toList } yield (s" + $i", x + i) // (String, Int) タプルの List を使って Writer モナド生成 writerT(list) } val w = writerT(("2", 2) :: Nil) >>= logManyNum(3) >>= logManyNum(2) //以下でも同じ //val w = writerT(List(("2", 2))) >>= logManyNum(3) >>= logManyNum(2) println(s"written = ${w.written}") println(s"value = ${w.value}") println(s"run = ${w.run}") }
実行結果は以下の通り。
ログと値がそれぞれ List になっており、run によって (ログ, 値) タプルの List を取得しています。
実行結果
scala> fits.sample.WriterTSample.main(null) written = List(2 + 1 + 1, 2 + 1 + 2, 2 + 2 + 1, 2 + 2 + 2, 2 + 3 + 1, 2 + 3 + 2) value = List(4, 5, 5, 6, 6, 7) run = List((2 + 1 + 1,4), (2 + 1 + 2,5), (2 + 2 + 1,5), (2 + 2 + 2,6), (2 + 3 + 1,6), (2 + 3 + 2,7))
ナイト移動の経路1
それでは Writer モナドの応用として、前回 id:fits:20120912 のナイト移動の処理に対して移動経路をログとして付けてみます。
まずは List モナドの要素を Writer モナドとする事で実装してみました。
moveKnight の引数を Writer モナドに変更し Writer モナドの List を返すように変更しています。 (ログは String で追記)
そして、start から x 手後に end に到達した際の経路を返す routeReachIn を追加しました。
MoveKnightWriter.scala
・・・ object MoveKnightWriter extends App { type KnightPos = Tuple2[Int, Int] type PosCalc = Function2[Int, Int, Int] // Writer モナドの生成 val createWriter = (p: KnightPos, sep: String) => writer (s"$sep$p", p) // 次の位置を設定した Writer モナドを取得 val nextPos = (a: Int, b: Int, fa: PosCalc, fb: PosCalc) => (p: KnightPos) => createWriter((fa(p._1, a), fb(p._2, b)), " -> ") val fl = List((_: Int) + (_: Int), (_: Int) - (_: Int)) val moveKnight = (p: Writer[String, KnightPos]) => ( for { a <- List(2, 1) b <- List(2, 1) fa <- fl fb <- fl if a != b } yield p >>= nextPos(a, b, fa, fb) ) filter { _.value match { case (x, y) => 1 <= x && x <= 8 && 1 <= y && y <= 8 } } val inMany = (x: Int) => (start: KnightPos) => { List(createWriter(start, "")) >>= List.fill(x){ Kleisli(moveKnight) }.reduceRight {(a, b) => b <=< a } } // start から x 手後に end へ到達した際の経路 (String の List) を取得 val routeReachIn = (x: Int) => (start: KnightPos, end: KnightPos) => inMany(x)(start).filter { _.value == end } map { _.written } routeReachIn(3)((6, 2), (6, 1)).foreach {println} println("-----") routeReachIn(3)((6, 2), (7, 3)).foreach {println} }
実行結果は以下の通り。
3手で (6, 2) から (6, 1) へ移動した際の経路が出力できており、(7, 3) への経路は無いため出力されていません。
実行結果
scala> fits.sample.MoveKnightWriter.main(null) (6,2) -> (8,1) -> (7,3) -> (6,1) (6,2) -> (4,1) -> (5,3) -> (6,1) (6,2) -> (7,4) -> (5,3) -> (6,1) (6,2) -> (7,4) -> (8,2) -> (6,1) (6,2) -> (5,4) -> (7,3) -> (6,1) (6,2) -> (5,4) -> (4,2) -> (6,1) -----
なお、moveKnight は以下のように 2段で for を使って実装することも可能です。
val moveKnight = (p: Writer[String, KnightPos]) => ( for { a <- List(2, 1) b <- List(2, 1) fa <- fl fb <- fl if a != b } yield { for { pv <- p np <- nextPos(a, b, fa, fb)(pv) } yield np } ) filter { ・・・ }
ナイト移動の経路2
次は WriterT.writerT() を使って List モナドの機能を持つ Writer モナドを使ってみます。
moveKnight の結果を WriterT[List, String, KnightPos] 用に変換し、Writer モナドを取得する moveKnightWriter 関数を別途用意してみました。
また、moveKnightWriter 関数を Kleisli に上手く適用する方法が分からなかったので *3、関数合成は >>= を使って自前で行っています。
MoveKnightWriterT.scala
・・・ object MoveKnightWriterT extends App { type KnightPos = Tuple2[Int, Int] val moveKnight = (p: KnightPos) => List( (p._1 + 2, p._2 - 1), (p._1 + 2, p._2 + 1), (p._1 - 2, p._2 - 1), (p._1 - 2, p._2 + 1), (p._1 + 1, p._2 - 2), (p._1 + 1, p._2 + 2), (p._1 - 1, p._2 - 2), (p._1 - 1, p._2 + 2) ).filter { case (x, y) => 1 <= x && x <= 8 && 1 <= y && y <= 8 } // Writer モナドを生成( WriterT[List, String, KnightPos] ) val moveKnightWriter = (p: KnightPos) => { // List[KnightPos] を List[Tupble2[String, KnightPos]] に変換 val list = moveKnight(p).map {np => (s" -> $np", np) } writerT(list) } val inMany = (x: Int) => (start: KnightPos) => { val stWriter = writerT((s"$start", start) :: Nil) // 以下でも同じ //val stWriter = writerT(List((s"$start", start))) stWriter >>= List.fill(x){ moveKnightWriter }.reduceRight {(a, b) => // Kleisli を使わずに関数合成 (p: KnightPos) => b(p) >>= a } } // start から x 手後に end へ到達した際の経路 (String の List) を取得 val routeReachIn = (x: Int) => (start: KnightPos, end: KnightPos) => inMany(x)(start).run.filter { _._2 == end } map { _._1 } routeReachIn(3)((6, 2), (6, 1)).foreach {println} println("-----") routeReachIn(3)((6, 2), (7, 3)).foreach {println} }
実行結果
scala> fits.sample.MoveKnightWriterT.main(null) (6,2) -> (8,1) -> (7,3) -> (6,1) (6,2) -> (4,1) -> (5,3) -> (6,1) (6,2) -> (7,4) -> (5,3) -> (6,1) (6,2) -> (7,4) -> (8,2) -> (6,1) (6,2) -> (5,4) -> (7,3) -> (6,1) (6,2) -> (5,4) -> (4,2) -> (6,1) -----