Scala
オブジェクト指向
デザインパターン
モナド
関数型プログラミング

Commandパターンをワンランク上の関数型の技法で置き換えてみる

"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パターンの構成要素と以下のように対応する。
- CashRegisterClient
- RegisterInvoker
- 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モナドに持たせるようにした。
  • vartotalを持っていた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を連鎖的に受け渡しているのがよくない。これを次に直す。

gist --- by writer monad

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 モナドの時はVectorMonoidとして扱われていたので、単位元を明示的に指定する必要がなかった。微妙な修正だが、次はこれをEffモナドを使って直してみる。

gist --- by state monad

Eff モナド

購入履歴をWriteに、最後のCashRegisterStateに持たせて、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))

WriterStateの両方のいいところが使えるようになった。(ただしこの例だと複雑性がまして難度が高くなったデメリットの方が大きいかもしれない。)

gist --- by eff monad

IOモナド

関数型プログラミングの観点で言えば、Purchaseprintlnを呼んでいるのもそもそも気になる。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()

printlnIOモナドに包むことはできたが、モナドがIOに固定されているのがまだいまいち。またmakePurchaseはドメインのコードなので、入出力や並列関連など「実行」にまつわるコードは切り離して、「関心事の分離」を徹底したい。

gist --- by io monad

Free モナド

  • PrintLnを含むConsoleというADTをリフト関数と共に定義。
  • makePurchaseでは、printlnの直接呼び出しでもIOモナドにくるんだ形でもなく、ADTFreeにリフトして返すようにした。
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)

ドメインのコードから「どのように動かすか」を分離して、後付けで任意のモナドを選択できるようにした。

gist --- by free monad

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")

gist --- by akka typed

TODO

  • tagless final、super monadの試行。
  • 書籍中の他のOOP置き換えパターンでも同様にやってみること。