Scala(Scalaz)のFreeモナドに関して勉強中なのですが、とりあえず現時点までに調べた内容と自分の考えについてまとめてみました。
具体的な用途
処理を順次実行するための用途が中心であると思われる。ElixirのEcto.Multiにちょっと似ている。(こちらのmichalmuskalaさんのコメントを見る限り、Ecto.MultiはFreeモナドのアイデアを参考にしているようである)
(2016/02/05追記)
下記の記事を拝見したのですが、Freeモナドは
データが末端に来る全ての再帰データ型に使えるモデル
であると理解しておくのがいいのかな?と。
独習 Scalaz — Stackless Scala with Free Monads
上記記事からの引用ですが、下記のように色々な再帰的データ構造が簡単に作れるようです。
type Pair[+A] = (A, A)
type BinTree[+A] = Free[Pair, A]
type Tree[+A] = Free[List, A]
type FreeMonoid[+A] = Free[({type λ[+α] = (A,α)})#λ, Unit]
type Trivial[+A] = Unit
type Option[+A] = Free[Trivial, A]
また、下記の記事では
- Functorがあれば任意のデータ型をMonadにできる
- Coyonedaを使えば Kind が * のものは、FunctorさえなくてもFree Monadにできる(それをOperational Monadと呼ぶらしい)
ある意味、IOモナドや、Readerモナドの代替というか、それらを組み合わせたような効果を表現できる(?)- FreeでDSLを作っておいて、それを実行するinterpreter4を切り替えることによって、とても綺麗に副作用を切り離すことができたり、副作用5の実行部分を超柔軟にあとから切り替えることができる(テストなどで便利)。DIフレームワークや、テスト用のモックライブラリなんていらなかったんや!
という説明も記載されていました。
CoproductとInjectを使ったFree Monadの合成とExtensible Effects - scalaとか・・・
サンプルコード1(インタープリタ方式)
下記のリポジトリに置いてあります。
kenta-polyglot/freemonad_sample
import scalaz.{Free, Functor, Id, Monad, ~>}
object Sample1 extends App {
trait Animal[+A]
case class Human(name: String) extends Animal[String]
case class Cheetah(kph: Int) extends Animal[Int]
case class Elephant(weight: Double) extends Animal[Double]
case class Monster(name: String, kph: Int, weight: Double) extends Animal[(String, Int, Double)]
def human(name: String): Free[Animal, String] =
Free.liftF[Animal, String](Human(name))
def cheetah(kph: Int): Free[Animal, Int] =
Free.liftF[Animal, Int](Cheetah(kph))
def elephant(weight: Double): Free[Animal, Double] =
Free.liftF[Animal, Double](Elephant(weight))
def monster(name: String, kph: Int, weight: Double): Free[Animal, (String, Int, Double)] =
Free.liftF[Animal, (String, Int, Double)](Monster(name, kph, weight))
val interpreter = new (Animal ~> Id.Id) {
def apply[A](a: Animal[A]): A = a match {
case Human(name) =>
println(s"I'm a human. My name is ${name}.")
name
case Cheetah(kph) =>
println(s"I'm a cheetah. I can run at ${kph} kph.")
kph
case Elephant(weight) =>
println(s"I'm an elephant. My weight is ${weight} ton.")
weight
case Monster(name, kph, weight) =>
println(s"I'm a monster. My name is ${name}. I can run at ${kph} kph. My weight is ${weight} ton. hahaha.")
(name, kph, weight)
}
}
val subs: Free[Animal, String] = for {
_ <- human("Kenta Katsumata")
kph <- cheetah(60)
weight <- elephant(2.5)
_ <- monster("Hulk", kph, weight)
} yield (s"complete!")
val result = subs.foldMap(interpreter)
println(result)
}
実行してみる
sbt "runMain Sample1"
// I'm a human. My name is Kenta Katsumata.
// I'm a cheetah. I can run at 60 kph.
// I'm an elephant. My weight is 2.5 ton.
// I'm a monster. My name is Hulk. I can run at 60 kph. My weight is 2.5 ton. hahaha.
// complete!
サンプルコード2(Functorとresumeと再帰を使用)
import scalaz.{Free, Functor, Id, Monad, -\/, \/-, ~>}
object Sample2 extends App {
trait Animal[+A]
// Functorの制約により、各case classにジェネリクスなメンバを含める必要がある
case class Human[A](name: String, a: A) extends Animal[A]
case class Cheetah[A](kph: Int, a: A) extends Animal[A]
case class Elephant[A](weight: Double, a: A) extends Animal[A]
case class Monster[A](name: String, kph: Int, weight: Double, a: A) extends Animal[A]
case class None() extends Animal[Nothing]
def human(name: String): Free[Animal, String] =
Free.liftF[Animal, String](Human(name, ""))
def cheetah(kph: Int): Free[Animal, Int] =
Free.liftF[Animal, Int](Cheetah(kph, 0))
def elephant(weight: Double): Free[Animal, Double] =
Free.liftF[Animal, Double](Elephant(weight, 0.0))
def monster(name: String, kph: Int, weight: Double): Free[Animal, (String, Int, Double)] =
Free.liftF[Animal, (String, Int, Double)](Monster(name, kph, weight, ("", 0, 0.0)))
implicit val animalFunctor = new Functor[Animal] {
def map[A, B](a: Animal[A])(f: A => B): Animal[B] = a match {
// いずれかのcase文をコメントアウトするとランタイムエラーが発生する
case Human(name, a) => Human(name, f(a))
case Cheetah(kph, a) => Cheetah(kph, f(a))
case Elephant(weight, a) => Elephant(weight, f(a))
case Monster(name, kph, weight, a) => Monster(name, kph, weight, f(a))
}
}
val subs: Free[Animal, String] = for {
_ <- human("Kenta Katsumata")
kph <- cheetah(60)
weight <- elephant(2.5)
_ <- monster("Hulk", kph, weight)
} yield (s"complete!")
def run(free: Free[Animal, String]):String =
free.resume match {
case \/-(a) => a
case -\/(Human(name, a)) => // いちいち受け取って・・・
println(s"I'm a human. My name is ${name}.")
run(a) // いちいち次の処理に渡す必要がある
case -\/(Cheetah(kph, a)) =>
println(s"I'm a cheetah. I can run at ${kph} kph.")
run(a)
case -\/(Elephant(weight, a)) =>
println(s"I'm an elephant. My weight is ${weight} ton.")
run(a)
case -\/(Monster(name, kph, weight, a)) =>
println(s"I'm a monster. My name is ${name}. I can run at ${kph} kph. My weight is ${weight} ton. hahaha.")
run(a)
case -\/(_) =>
"not exists."
}
run(subs)
}
実行してみる
sbt "runMain Sample2"
// I'm a human. My name is Kenta Katsumata.
// I'm a cheetah. I can run at 60 kph.
// I'm an elephant. My weight is 2.5 ton.
// I'm a monster. My name is Hulk. I can run at 0 kph. My weight is 0.0 ton. hahaha.
所感
Ecto.Multi
のように、処理を一気に順次実行するための用途でFreeモナドを使う場合、Functorとresumeを使う方式は余計なコードが増えてしまうのであまり望ましくないような印象。下記のスライド等を拝見する限り、「なにかの再帰処理を、綺麗に、安全に、かつスタックオーバーフローを発生させないように書きたい」という場合、もしくは「順次処理の途中で処理待ち(ユーザの入力待ち等)が発生する場合」に、Functorとresumeを使うと便利なのかもしれない。(未検証)
stackless scala with free monad
備考
参考にさせて頂いたサンプルコードの多くは、Functorとresumeを使う方式で記述されていたのですが、これは旧バージョンのscalazのFreeモナドの各畳み込み関数等の引数がimplicit
なFunctor
を要求していたことによるものなのかもしれません。
scalaz/Free.scala at series/7.1.x · scalaz/scalaz
バージョン7.3のfoldMap
関数等は、暗黙のFunctor
引数が削除されているので、「順次処理が目的の場合はFunctorいらないよね」みたいな議論があったのかなーみたいな。
参考にさせて頂いた資料
stackless scala with free monad
独習 Scalaz — Free Monad
独習 Scalaz — Stackless Scala with Free Monads
Freeモナド in Scala - ( ꒪⌓꒪) ゆるよろ日記
scalaz - Free - Qiita
Free Monads in Scalaz - how to use them - Chris Stucchio