"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置き換えパターンでも同様にやってみること。