先日見かけた Closeモナド についての言及になります
この記事の概要
長くなりそうなので最初にまとめです
-
Close
モナドにはいくつかの問題があります - それらの問題は既存の継続モナドによって解決できます
- リソース管理のために新たなモナドを考案する必要はありません
Close モナドが抱える問題
動作の問題
-
run
を二回以上呼べません- リソースがインスタンス変数として保持されているためです
- 言い換えると
map
やflatMap
自体に副作用があります
コードの問題
-
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)
}
}
}
Callback
は Continue
で表現できることが分かったのでまとめます :
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
}
}
}
この変更によって直接 run
が A
の値を返せるようになります。
もうコールバック関数の存在すら意識する必要はありません。やった!
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 実装がとてもおもしろいです
- Tonny Morris さんによる記事です