はじめに
Scalaで一番よく使うローンパターンでは、ローンパターンの典型的なコードとして次があげられている。
import java.io.Writer
import scala.io.Source
object Using {
def apply[A, B](resource: A)(process: A => B)(implicit closer: Closer[A]): B =
try {
process(resource)
} finally {
closer.close(resource)
}
}
case class Closer[-A](close: A => Unit)
object Closer {
implicit val sourceCloser: Closer[Source] = Closer(_.close())
implicit val writerCloser: Closer[Writer] = Closer(_.close())
}
このコードは、リソース(resource)を使った関数processを受け取ってそれを実行する。もしprocessが成功したとしても、あるいは失敗して例外を送出したとしても、リソースを閉じるためにcloser.close(resource)を呼び出すようになっている。ただ、このコードは著者が主張するようにモナドではないため、for式の中で使うことができない。よって、たとえば次のようにいくつものリソースを取り扱う場合はネストする。
Using(new FileInputStream(getClass.getResource("/source.txt").getPath)) { in =>
Using(new InputStreamReader(in, "UTF-8")) { reader =>
Using(new BufferedReader(reader)) { buff =>
Using(new FileOutputStream("dest.txt")) { out =>
Using(new OutputStreamWriter(out, "UTF-8")) { writer =>
var line = buff.readLine()
while (line != null) {
writer.write(line + "\n")
line = buff.readLine()
}
}
}
}
}
}
また、Loanパターンをモナドfor式で使えるようにしてみたよでは次のようにしてfor式の中で使えるようにしている。
class Loan[T <: {def close()}] private (value: T) {
def foreach[U](f: T => U): U = try {
f(value)
} finally {
value.close()
}
}
object Loan {
def apply[T <: {def close()}](value: T) = new Loan(value)
}
これを用いると先ほどのネストした例を次のように書ける。
for {
in <- Loan(new FileInputStream("source.txt"))
reader <- Loan(new InputStreamReader(in, "UTF-8"))
buff <- Loan(new BufferedReader(reader))
out <- Loan(new FileOutputStream("dest.txt"))
writer <- Loan(new OutputStreamWriter(out, "UTF-8"))
} {
var line = buff.readLine()
while (line != null) {
writer.write(line)
line = buff.readLine()
}
}
ただ、この例では著者が主張するようにモナドにはなっていない。本記事ではこのようなIOのリソースを適切にクローズするようなCloseモナド1の作成を行う。また作成したモナドに対してscalapropsでテストを作成する。なお、全体のソースコードは次のリポジトリにある。
追記
@jwhaco さんが継続モナドを利用してよりよい実装を公開されていましたので、紹介させていただきます。
Closeモナド
このモナドの作成はリーダーモナドとFujiTaskを参考にした。
abstract class Close[+R, +A](res: R) { self =>
protected def process()(implicit closer: Closer[R]): A
def run()(implicit closer: Closer[R]): A =
try {
process()
} finally {
closer.close(res)
}
def flatMap[AR >: R, B](f: A => Close[AR, B]): Close[AR, B] = new Close[AR, B](res) {
def process()(implicit closer: Closer[AR]): B =
try {
f(self.process()).process()
} finally {
closer.close(res)
}
override def run()(implicit closer: Closer[AR]): B =
process()
}
def map[B](f: A => B): Close[R, B] = flatMap(x => Close(res, f(x)))
}
object Close {
def apply[R, A](res: R, a: => A) = new Close[R, A](res) {
def process()(implicit closer: Closer[R]): A = a
}
def apply[R](res: R): Close[R, R] = apply(res, res)
}
trait Closer[-A] {
def close(a: A): Unit
}
object Closer {
def apply[A](f: A => Unit): Closer[A] = new Closer[A] {
def close(a: A): Unit = f(a)
}
}
まずCloseモナドは2つの型パラメータを受け取る。型パラメータRはリソースの型を表し、型パラメータAは結果の型を表すようになっている。 また、flatMapの内部では新しいCloseモナドを作成している。processメソッドを積んでいく構造になっており、まず自分(self)のprocessメソッドを実行し、その結果をfに投入してさらにprocessメソッドを呼ぶようになっている。この一連の実行はtryの中に入れることで、成功したとしても失敗して例外が送出されたとしてもfinallyでcloser.close()が実行されリソースがクローズされるようにしている。 closerはリソースの型Rに対応するリソースをクローズする方法を提供する型クラスのインスタンスである。型クラスCloser[A]はリソースAをクローズするためのメソッドcloseを持つ。
また、がくぞさんから指摘を参考に次の2つの変更を与えた。
res: Rをres: => Rにしてリソースの掴みっぱなしを無くした
- kawachiさんの指摘を受けて
res: Rへ戻した
runメソッドは最初だけリソースを解放するようにセットしておき、一度でもmap/flatMapが発生するとprocessを呼び出すだけになるようにした。こうすることで指摘にあった未合成のCloseモナドのrunでリソースリークする問題に対応した
Closeモナドの実行例
さきほどの例をCloseモナドで書くと次のようになる。
implicit def closer[R <: Closeable]: Closer[R] = Closer { x =>
println(s"close: ${x.toString}")
x.close()
}
(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]")
}).run()
実行すると次のような結果が得られる2。
[begin]
This
is
a
pen
[end]
close: java.io.OutputStreamWriter@46cd1743
close: java.io.FileOutputStream@513460bd
close: java.io.BufferedReader@35f61ec8
close: java.io.InputStreamReader@56d5bf00
close: java.io.FileInputStream@788ccd96
scalapropsによるテスト
やや複雑になったのでGitHubにあるコードのリンクを貼ることにする。
まずClose[R, A]のRを何か適当に固定してscalazのモナドインスタンスを作成する。そして後はひたすらGenとEqualのインスタンスを作ればよい。ただ、Gen[Close[R, A]]の定義において、特定の割合でrunメソッドが例外を送出するような工夫を行った。 また、モナドの性質とは別に次のようなテストも追加した。
- 合成する前のリソースを
runした場合にきちんとクローズされるか res1、res2の順で合成した場合にres2、res1の順でクローズされるか
まとめ
この記事ではIOのリソースをクローズするCloseモナドを実装した。よい悪いの議論は別として、Closeモナドに限らず世の中にあるローンパターンはモナドで書き換え可能であるのではないかと考えている。Closeモナド以外にも、何かモナドで書き換えると便利になるような例があるかもしれない。
コメント
コメントを投稿