"Functional Programming Patterns in Scala and Clojure"という本(PDF)に、オブジェクト指向のデザインパターンやイディオムを関数型の技法で置き換える11個のイディオムが紹介されている。
ただ出版から4年くらい経ってて少し古いし、そもそもOOPメインのプログラマ向けに初歩のFP技法を導入するような本の趣旨なので、ある程度Scalaに慣れてくると置き換え結果のコードが若干物足りなくなる。
そこで、その中の一つの「Replacing Command(Commandパターンの置き換え)」を題材に、初級よりはやや難しめと思われるFP技法でさらに書き換えてみた。
道具立てとしては、Writer モナド、State モナド、Eff モナド、IO モナド、Free モナド、モナドトランスフォーマー、Akkaなどを使った。
ライブラリ等
下記あたり。
compilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full),
compilerPlugin("org.spire-math" %% "kind-projector" % "0.9.4"),
"org.typelevel" % "cats-core_2.12" % "1.0.0-RC1",
"org.typelevel" % "cats-free_2.12" % "1.0.0-RC1",
"org.typelevel" %% "cats-effect" % "0.5",
"org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
"org.atnos" %% "eff" % "5.0.0-20171107064756-b526890",
"com.typesafe.akka" %% "akka-typed" % "2.5.8"
書籍オリジナル
本に載ってた以下のScalaコードをお題にした。
class CashRegister(var total: Int) {
def addCash(toAdd: Int) {
total += toAdd
}
}
object Register {
def makePurchase(register: CashRegister, amount: Int) = {
() => {
println(s"Purchase in amount: $amount")
register.addCash(amount)
}
}
var purchases: Vector[() => Unit] = Vector()
def executePurchase(purchase: () => Unit) = {
purchases = purchases :+ purchase
purchase()
}
}
Commandパターンの構成要素と以下のように対応する。
-
CashRegister:Client -
Register:Invoker -
Register#makePurchaseが返す()=>Unit型の関数:Command
GoFのCommandパターンと微妙な差異があるが、「要求をオブジェクト化」して履歴として持ったりすることなどは共通。
書籍とほぼ同じように、これを以下のようにして動かす。
val register = new CashRegister(0)
val p1 = makePurchase(register, 100)
val p2 = makePurchase(register, 50)
executePurchase(p1)
executePurchase(p2)
register.total
register.total = 10
purchases.foreach(_())
register.total
- 残高ゼロのキャッシュレジスターを生成する
- このキャッシュレジスターを参照する購入関数オブジェクトを2個作る
- 関数オブジェクトを実行すると、購入と残高が出力されると共に、履歴が記録される
- キャッシュレジスターの残高を適当に変更する
- 履歴を再実行すると改めて購入の様子とともに再計算された残高が出力される
以下のようなコンソール出力になる。
Purchase in amount: 100
res0: Unit = ()
Purchase in amount: 50
res1: Unit = ()
res2: Int = 150
register.total: Int = 10
Purchase in amount: 100
Purchase in amount: 50
res3: Unit = ()
res4: Int = 160
書き直した結果
本では敷居の高い"purely functional"な技法を避けて"more pragmatic"な手法を取ったとあり、副作用を許容したコードなっている。ただ最近では関数型の技法も普及していることもあるので、ここではもうちょっと進んだ技法を使って純粋関数型なコーディングに寄せてみる。まず Writerモナドから。
Writerモナド
-
Vectorへのミュータブルな参照で保持していた購入履歴を、Writerモナドに持たせるようにした。 -
varでtotalを持っていたCashRegisterをイミュータブルにした。
case class CashRegister(total: Int) {
def addCash(toAdd: Int) = CashRegister(total + toAdd)
}
type Purchase = CashRegister => CashRegister
def makePurchase(amount: Int): Purchase = (r: CashRegister) => {
println(s"Purchase in amount: $amount")
r addCash amount
}
type PurchaseHistory = Vector[Purchase]
def execute(purchase: Purchase)
: CashRegister => Writer[PurchaseHistory, CashRegister] =
register => purchase(register).writer(Vector(purchase))
以下のコードで、オリジナルと同等の動作結果が得られる。
val p1 = makePurchase(100)
val p2 = makePurchase(50)
def program(r0: CashRegister) = for {
r1 <- execute(p1)(r0)
r2 <- execute(p2)(r1)
} yield r2
val (history, total1) = program(CashRegister(0)).run
// 再実行
history.foldLeft(CashRegister(10))((acc, p) => p(acc))
"impure"な変数はなくなったが、execute関数呼び出しの間でCashRegisterを連鎖的に受け渡しているのがよくない。これを次に直す。
Stateモナド
購入の実行ごとに更新されるCashRegisterを、Vector[Purchase]で表した購入履歴と共にStateに持たせるようにした。
case class CashRegister(total: Int) {
def addCash(toAdd: Int) = CashRegister(total + toAdd)
}
type Purchase = CashRegister => CashRegister
def makePurchase(amount: Int): Purchase = (r: CashRegister) => {
println(s"Purchase in amount: $amount")
r addCash amount
}
type PurchaseHistory = Vector[Purchase]
def execute(p: Purchase): State[(PurchaseHistory, CashRegister), Unit] =
State { case (h, r) => ((h :+ p, p(r)), ()) }
以下のように動かす
val p1 = makePurchase(100)
val p2 = makePurchase(50)
def program = for {
_ <- execute(p1)
_ <- execute(p2)
} yield ()
val (history, total1) = program.runS((Vector.empty, CashRegister(0))).value
// 再実行
history.foldLeft(CashRegister(10))((acc, p) => p(acc))
CashRegisterの連鎖的な受け渡しは解消されたが、Vector.emptyを渡すことになったのがやや残念。Writer モナドの時はVectorが Monoidとして扱われていたので、単位元を明示的に指定する必要がなかった。微妙な修正だが、次はこれをEffモナドを使って直してみる。
Eff モナド
購入履歴をWriteに、最後のCashRegisterをStateに持たせて、2種類のモナドを合成してみた。モナドトランスフォーマーを使うこともできたが、ここではEffを使った。
case class CashRegister(total: Int) {
def addCash(toAdd: Int) = CashRegister(total + toAdd)
}
type Purchase = CashRegister => CashRegister
def makePurchase(amount: Int): Purchase = (r: CashRegister) => {
println(s"Purchase in amount: $amount")
r addCash amount
}
type Stack = Fx.fx2[State[CashRegister, ?], Writer[Purchase, ?]]
type _writerVector[R] = Writer[Purchase, ?] |= R
type _stateRegister[R] = State[CashRegister, ?] |= R
def execute[R :_stateRegister :_writerVector](p: Purchase): Eff[R, Unit] = for {
_ <- tell(p)
_ <- modify(p(_))
} yield ()
以下のように動かす.
val p1 = makePurchase(100)
val p2 = makePurchase(50)
def program = for {
_ <- execute[Stack](p1)
_ <- execute[Stack](p2)
} yield ()
val ((_, last), history) = program.runState(CashRegister(0)).runWriter.run
// 再実行
val total2 = history.foldLeft(CashRegister(10))((acc, p) => p(acc))
WriterとStateの両方のいいところが使えるようになった。(ただしこの例だと複雑性がまして難度が高くなったデメリットの方が大きいかもしれない。)
IOモナド
関数型プログラミングの観点で言えば、Purchaseがprintlnを呼んでいるのもそもそも気になる。IOモナドとStateをモナドトランスフォーマーで組み合わせてunsafeなコードを局所化してみる。
case class CashRegister(total: Int) {
def addCash(toAdd: Int) = CashRegister(total + toAdd)
}
type Purchase = CashRegister => IO[CashRegister]
def makePurchase(amount: Int): Purchase = (r: CashRegister) => for {
_ <- IO { println (s"Purchase in amount: $amount") }
} yield r addCash amount
type PurchaseHistory = Vector[Purchase]
def execute(p: Purchase): StateT[IO, (PurchaseHistory, CashRegister), Unit] =
StateT { case (h, r) => p(r) map { r2 => ((h :+ p, r2), ()) }}
以下のコードでこれまで同様の動きとなる。StateTから取り出したIOを、ここではunsafeRunSync で実行しているが、他にも各種runメソッドが提供されていて、状況に応じて選択できる。
val p1 = makePurchase(100)
val p2 = makePurchase(50)
def program = for {
_ <- execute(p1)
_ <- execute(p2)
} yield ()
val io1 = program.runS((Vector.empty, CashRegister(0)))
val (history, last) = io1.unsafeRunSync()
val io2 = history.foldLeft(IO.pure(CashRegister(10)))(_ flatMap _)
io2.unsafeRunSync()
printlnをIOモナドに包むことはできたが、モナドがIOに固定されているのがまだいまいち。またmakePurchaseはドメインのコードなので、入出力や並列関連など「実行」にまつわるコードは切り離して、「関心事の分離」を徹底したい。
Free モナド
-
PrintLnを含むConsoleというADTをリフト関数と共に定義。 -
makePurchaseでは、printlnの直接呼び出しでもIOモナドにくるんだ形でもなく、ADTをFreeにリフトして返すようにした。
sealed trait Console[A]
case class PrintLn(s: String) extends Console[Unit]
type ConsoleF[T] = Free[Console, T]
def printLn(s: String): ConsoleF[Unit] = liftF(PrintLn(s))
case class CashRegister(total: Int) {
def addCash(toAdd: Int) = CashRegister(total + toAdd)
}
type Purchase = CashRegister => ConsoleF[CashRegister]
def makePurchase(amount: Int): Purchase = r => for {
_ <- printLn(s"Purchase in amount: $amount")
} yield r addCash amount
type PurchaseHistory = Vector[Purchase]
def execute(p: Purchase): StateT[ConsoleF, (PurchaseHistory, CashRegister), Unit] =
StateT { case (h, r) => p(r) map { r2 => ((h :+ p, r2), ()) }}
動かすコードは以下。基本的には同じ動きだが、ADTを解釈するインタープリター(自然変換)を、IO用とID用の2種類書いた。
val p1 = makePurchase(100)
val p2 = makePurchase(50)
def freeProgram = (for {
_ <- execute(p1)
_ <- execute(p2)
} yield ()).runS((Vector.empty, CashRegister(0)))
def ioCompiler: (Console ~> IO) = new (Console ~> IO) {
override def apply[A](fa: Console[A]): IO[A] = fa match {
case PrintLn(text) => IO { println(text) }
}
}
val (history, last) = freeProgram.foldMap(ioCompiler).unsafeRunSync()
// 再実行
def idCompiler: (Console ~> Id) = new (Console ~> Id) {
override def apply[A](fa: Console[A]): Id[A] = fa match {
case PrintLn(text) => println(text); ()
}
}
history.foldLeftM(CashRegister(10))((b, p) => p(b)).foldMap(idCompiler)
ドメインのコードから「どのように動かすか」を分離して、後付けで任意のモナドを選択できるようにした。
Akka typed
「データと手続きの一体化」のようなOOPバイアスを一旦カッコ入れしてちょっと考え直してみると、Purchaseが振る舞いを持ってCashRegisterを更新するのも不自然だし、Purchaseの責務ではないとすればCashRegister自身の責務のような気もする(OOP的にも)。
以下では、Purchaseを代数的データ型Commandに属する、単なる一つデータコンテナとして定義して、CashRegisterに振る舞いを移動し、client(CashRegister)とinvokerの「協調」を Akka Actorの通信として表現してみる。
trait Command
final case class Purchase(amount: Int) extends Command
case object PrintTotal extends Command
object CashRegister {
def apply(total: Int): Behavior[Command] = behavior(total)
private def behavior(total: Int): Behavior[Command] = immutable {
case (_, Purchase(amount)) => println(amount); behavior(total + amount)
case (_, PrintTotal) => println(total); Actor.same
}
}
type PurchaseHistory = Vector[Purchase]
def execute(p: Purchase)
: ReaderT[WriterT[IO, PurchaseHistory, ?], ActorRef[Command], Unit] =
ReaderT { actor =>
WriterT.lift[IO, PurchaseHistory, Unit](IO { actor ! p }) tell Vector(p)
}
Actor以外の道具立てとしては、ReaderT, WriterT, IOをモナドトランスフォーマーで組み合わせている。WriterTにはこれまでと同じように購入履歴を持たせているが、ReaderにはActorRef を載せている。またIOを使って記述と実行を分離している。
以下のコードでこれまでとほぼ同じ動作になるが、CashRegisterの残額を表示するのにPrintTotalコマンドを送信しているところが少し違う。
val p1 = Purchase(100)
val p2 = Purchase(50)
def program = for {
_ <- execute(p1)
_ <- execute(p2)
} yield ()
val root = Actor.deferred[Nothing] { ctx =>
val (h, _) = program.run(register).run.unsafeRunSync()
register ! PrintTotal
val register2 = ctx.spawn(CashRegister(1), "register2")
h.foreach(a => register2 ! a)
register2 ! PrintTotal
Actor.empty
}
ActorSystem[Nothing](root, "RegisterSample")
TODO
- tagless final、super monadの試行。
- 書籍中の他のOOP置き換えパターンでも同様にやってみること。