Scala

Loan パターンのネストは継続モナドでシュッと解決できるよという話

先日見かけた Closeモナド についての言及になります


この記事の概要

長くなりそうなので最初にまとめです



  • Close モナドにはいくつかの問題があります

  • それらの問題は既存の継続モナドによって解決できます

  • リソース管理のために新たなモナドを考案する必要はありません


Close モナドが抱える問題

動作の問題



  • run を二回以上呼べません


    • リソースがインスタンス変数として保持されているためです

    • 言い換えると mapflatMap 自体に副作用があります



コードの問題



  • Close[R, A]A にアクセスするための手段が map しかありません


    • 副作用が目的の処理であっても map が必要になってしまいます



  • クラス内にリソースを閉じるための try-finally が二回書かれています


    • DRY に書きたいですね




継続モナドの導入

まずは最も簡潔な(と思われる)実装を先に載せておきます

package continuation

class Continue0[+A] private(callback: (A => Unit) => Unit) {

def map[B](f: A => B): Continue0[B] = Continue0 {
g => apply(f andThen g)
}
def flatMap[B](f: A => Continue0[B]): Continue0[B] = Continue0 {
g => apply(a => f(a) apply g)
}
def apply(f: A => Unit): Unit = callback(f)

def run(): Unit = callback(_ => ())
}
object Continue0 {
def apply[A](f: (A => Unit) => Unit): Continue0[A] = {
new Continue0(f)
}
def from[A](a: A): Continue0[A] = {
new Continue0[A](f => f(a))
}
}

呼び出し側のコードはこんな感じです

  def sample0() = {

val continue = for {
x1 <- Continue0 from 1
x2 <- Continue0 from 2
x3 <- Continue0 from 3
} yield {
x1 - x2 - x3
}
continue { n =>
println(n)// -4
}
}

for-yield を展開してみましょう。綺麗なネストの出現を確認できます

  def sample1() = {

val continue =
Continue0 from 1 flatMap { x1 =>
Continue0 from 2 flatMap { x2 =>
Continue0 from 3 map { x3 =>
x1 - x2 - x3
}
}
}
continue { n =>
println(n)// -4
}
}

なんとなく形が見えてきましたね


継続モナドを利用して Close を実装する

といっても、コールバック関数を実行してリソースを閉じるだけです

package continuation

// Closer は元記事と同じ
import close.Closer

object Close {
def apply[A: Closer](res: => A): Continue0[A] =
Continue0 { callback =>
val target = res
try callback(target)
finally implicitly[Closer[A]] close target
}
}

念のため使用例も見てみましょう

  def main(args: Array[String]): Unit = {

implicit def closer[R <: Closeable]: Closer[R] = Closer { x =>
println(s"close: ${x.toString}")
x.close()
}
val a = for {
in <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
reader <- Close(new InputStreamReader(in, "UTF-8"))
buff <- Close(new BufferedReader(reader))
out <- Close(new FileOutputStream("dest.txt"))
writer <- Close(new OutputStreamWriter(out, "UTF-8"))
} yield {
println("[begin]")

var line = buff.readLine()
while (line != null) {
println(line)

writer.write(line + "\n")
line = buff.readLine()
}
println("[end]")
}
a.run()
a.run() // 二回目
}

問題なさそうです。run も無事に二回呼べています。

[begin]

This
is
a
pen
[end]
close: java.io.OutputStreamWriter@400f2b73
close: java.io.FileOutputStream@13150587
close: java.io.BufferedReader@33ddbb85
close: java.io.InputStreamReader@122c984a
close: java.io.FileInputStream@2bc16dcf
[begin]
This
is
a
pen
[end]
close: java.io.OutputStreamWriter@10436c0d
close: java.io.FileOutputStream@4e1cec13
close: java.io.BufferedReader@3804d352
close: java.io.InputStreamReader@188b6e88
close: java.io.FileInputStream@13234a90

もう for-yield の最後の値を得るために map を呼ぶ必要もありません

    val foo = for {

in <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
reader <- Close(new InputStreamReader(in, "UTF-8"))
buff <- Close(new BufferedReader(reader))
out <- Close(new FileOutputStream("dest.txt"))
writer <- Close(new OutputStreamWriter(out, "UTF-8"))
} yield {
"hello"
}
foo {
message => println(message) // hello
}

これで Close 自体のリファクタは終わりです。おつかれさまでした。


継続モナドを改善してコールバック関数を不要にする

簡単のため省きましたが、前述の継続モナドにはまだ不自然な i/f が残っています。

class Continue0[+A] private(callback: (A => Unit) => Unit) {

(A => Unit) => (Unit)Unit って何だよ!そこの制限を外せば値を直接返せるじゃん!って思いますよね。思わなかったら今から思ってください。

しかしながら、ここで何も考えずに愚直に型パラメータを導入しても :

package continuation

class Continue1[R, +A] private(callback: (A => R) => R) {

def map[B](f: A => B): Continue1[R, B] = Continue1 {
g => apply(f andThen g)
}
def flatMap[B](f: A => Continue1[R, B]): Continue1[R, B] = Continue1 {
g => apply(a => f(a) apply g)
}
def apply(f: A => R): R = callback(f)

/*
def run(): Unit = callback(_ => ())

[error] found : Unit
[error] required: R
[error] def run(): Unit = callback(_ => ())
[error] ^
*/
}

run で上記のように叱られます。

callback の実行でしか必要のないはずの型 R をクラスに保持させているのが原因ですね。

ということで、多相の apply を得るために callback に専用の型を与えてしまいましょう。

package continuation

trait Callback[A] {
def apply[R](f: A => R): R
}
class Continue[+A] private(callback: Callback[A]) {

def map[B](f: A => B): Continue[B] =
Continue apply new Callback[B] {
override def apply[R](g: B => R): R = {
callback(f andThen g)
}
}
def flatMap[B](f: A => Continue[B]): Continue[B] =
Continue apply new Callback[B] {
override def apply[R](g: B => R): R = {
callback(a => f(a) apply g)
}
}
def apply[R](f: A => R): R = callback(f)
def run(): A = apply(identity)
}
object Continue {
def apply[A](f: Callback[A]): Continue[A] = {
new Continue(f)
}
def from[A](a: A): Continue[A] = {
Continue apply new Callback[A] {
override def apply[R](f: A => R): R = f(a)
}
}
}

CallbackContinue で表現できることが分かったのでまとめます :

trait Continue[A] {

self =>
def map[B](f: A => B): Continue[B] = new Continue[B] {
override def apply[R](g: B => R): R = self(f andThen g)
}
def flatMap[B](f: A => Continue[B]): Continue[B] = new Continue[B] {
override def apply[R](g: B => R): R = self(a => f(a) apply g)
}
def apply[R](f: A => R): R
def run(): A = apply(identity)
}

object Continue {
def from[A](a: A): Continue[A] = {
new Continue[A] {
override def apply[R](f: A => R): R = f(a)
}
}
}

Close も少しゴツい実装に変わりますが… :

object Close {

def apply[A: Closer](res: => A): Continue[A] =
new Continue[A] {
override def apply[R](f: A => R): R = {
val target = res
try f(target)
finally implicitly[Closer[A]] close target
}
}
}

この変更によって直接 runA の値を返せるようになります。

もうコールバック関数の存在すら意識する必要はありません。やった!

    val foo = for {

in <- Close(new FileInputStream(getClass.getResource("/source.txt").getPath))
reader <- Close(new InputStreamReader(in, "UTF-8"))
buff <- Close(new BufferedReader(reader))
out <- Close(new FileOutputStream("dest.txt"))
writer <- Close(new OutputStreamWriter(out, "UTF-8"))
} yield {
...
"hello"
}
println(foo.run()) // "hello"

これで一通りの i/f 改善も終わりです。今度こそおつかれさまでした。


参考リンク



  • Continuation monad in Scala


    • Tonny Morris さんによる記事です


      • 投稿は 2008 年ですが概念は今でも変わっていません

      • 処理を分岐させる callcc 実装がとてもおもしろいです