はじめに
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
モナド以外にも、何かモナドで書き換えると便利になるような例があるかもしれない。