ちょっと前に、Continuationモナドの勉強ってことでファイル入出力の例を写経しつつ、自分でも少し書いてみました。今度は、for式に使えるクラスを自分で書けるようになることも目的にしつつ、もうちょっと実装難易度を下げてfor式で入出力を扱えるようなプログラムを書いてみたいと思います。
今回の目標は
- 入出力が重なっても、ネストさせない
- for式で扱えるクラスを書く
ということです。
目標としては
for { fis <- new FileInputStream("...") isr <- new InputStreamReader(fis, "UTF-8") reader <- new BufferedReader(isr) } { // 何か… }
のようなコードが書けるようなプログラムを書くことです。Scalaでよくあるusingの例だと、必ずリソースごとにネストしますからね…。
簡単に使えるようにしたいのですが、暗黙の型変換はちょっと避けられないでしょうね。まあ、変換がかかっていること自体は明示したいので、まずはこんなトレイト&クラスを用意しました。
trait ResourceBridge[+A] { def toR: Resource[A] } abstract class Resource[+A](r: A) { // 省略 }
実際のリソース管理を行うクラスがResourceクラスで、その型変換の際のブリッジとなるのがResourceBridgeトレイトです。実装は、各サブクラスで行います。
ここで、よく出てくるのがStructual Subtypingですがこれは継承などのクラスの関係に弱く(*パターンマッチには使えない、など)、1代限りとなってしまうので、もっと具象なインターフェースなどに依存するようにしました。
例えば、java.io.Closeableインターフェースを相手にする場合は、こんな感じで。
import java.io.Closeable class IoCloseableResourceBridge[+A <: Closeable](r: A) extends ResourceBridge[A] { def toR: Resource[A] = new IoCloseableResource(r) } class IoCloseableResource[+A <: Closeable](r: A) extends Resource[A](r) { // 省略 }
ここで、Resourceクラスをfor式で使えるようにしたいということでforeach、map、flatMapを実装し、サブクラスが特定のクラスでパラメータ化されることを考えリソースを閉じる部分を内部のトレイトに切り出しました。
結果、こんな実装になりました。
trait ResourceBridge[+A] { def toR: Resource[A] } abstract class Resource[+A](r: A) { protected trait Disposable { val actual: A = r def dispose(): Unit } protected val disposable: Disposable protected def wrap[B](r: B): Resource[B] = Resource.select(r) def opened[B](f: A => B): B = f(disposable.actual) def use[B](f: A => B): B = try { f(disposable.actual) } finally { dispose() } def map[B](f: A => B): Resource[B] = wrap(use(f)) def flatMap[B](f: A => Resource[B]): Resource[B] = wrap(use(x => f(x).disposable.actual)) def foreach(f: A => Unit): Unit = use(f) def get: A = this match { case n: NoManageResource[_] => disposable.actual case _ => throw new UnsupportedOperationException("Not Support get method.") } final def dispose(): Unit = disposable.dispose() }
useメソッドはリソースを貸し出すローンパターンになっていて、終了後必ずfinalyで閉じます。外から明示的に扱いたい場合は、openedとdisposeをそれぞれ呼び出します。
mapメソッドおよびflatMapメソッドは、戻り値がResourceクラスとなるので用途に合ったResourceクラスのサブクラスを戻してあげる必要があります。ここで、どういうケースでどういうResourceクラスのサブクラスを選択すればよいかは、Resourceコンパニオンオブジェクトに持たせることにしました。
Resourceコンパニオンオブジェクトの実装は、こちら。
import java.io.Closeable object Resource { private var resourceFactory: PartialFunction[Any, Resource[_]] = ioCloseableToResource def addResourceFactory(f: PartialFunction[Any, Resource[_]]): Unit = resourceFactory = resourceFactory orElse f def anyToResource: PartialFunction[Any, Resource[_]] = { case any => new NoManageResource(any) } implicit def closeableToIoCloseableBridge[A <: Closeable](c: A): IoCloseableResourceBridge[A] = new IoCloseableResourceBridge(c) def ioCloseableToResource: PartialFunction[Any, Resource[Closeable]] = { case c: Closeable => new IoCloseableResource(c) } def select[A](r: A): Resource[A] = resourceFactory.orElse(anyToResource)(r).asInstanceOf[Resource[A]] }
selectメソッドで、引数に応じたResourceを選択して返します。とはいっても、selectメソッドが受け取るのは、yieldの最後の値なので大抵の場合は最後のケース(anyToResource)に落ちます。その場合のクラスは、すぐ登場します。あと、デフォルトではCloseableに対応したimplicit conversionを定義しています。
Resourceクラスのサブクラスの完全な実装は、こちらです。
// IoCloseableResource.scala import java.io.Closeable class IoCloseableResourceBridge[+A <: Closeable](r: A) extends ResourceBridge[A] { def toR: Resource[A] = new IoCloseableResource(r) } class IoCloseableResource[+A <: Closeable](r: A) extends Resource[A](r) { protected val disposable = new Disposable { def dispose(): Unit = r.close() } } // NoManageResource.scala final class NoManageResourceBridge[+A](r: A) extends ResourceBridge[A] { def toR: Resource[A] = new NoManageResource(r) } final class NoManageResource[+A](r: A) extends Resource[A](r) { protected val disposable = new Disposable { def dispose: Unit = { } } }
特にリソース管理を行わないのが、NoManageResourceクラスです。yieldで返す値が、Resourceコンパニオンオブジェクトに登録されたどのリソース管理クラスへの変換ルールにもマッチしなかった場合は、このクラスに変換されます。
では、使ってみましょう。
import java.io.{BufferedReader, FileInputStream, InputStreamReader} import Resource._ object ResourceTest { def main(args: Array[String]): Unit = { val contents = for { fis <- new FileInputStream("src/main/scala/ResourceTest.scala").toR isr <- new InputStreamReader(fis, "UTF-8").toR reader <- new BufferedReader(isr).toR } yield { def readLines(reader: BufferedReader, lines: List[String]): List[String] = reader.readLine() match { case null => lines reverse case line => readLines(reader, line :: lines) } readLines(reader, Nil) } contents.get.foreach(println) } }
これを実行すると、このテストコード自体がコンソールに出力されます。
for式で
fis <- new FileInputStream("src/main/scala/ResourceTest.scala").toR isr <- new InputStreamReader(fis, "UTF-8").toR reader <- new BufferedReader(isr).toR
implicit conversion経由で、Resourceクラスに各Stream、Readerを変換しています。型推論は、一応活用できています。
yieldで評価した結果は、for式の外に持ち出せるようになっています。
contents.get.foreach(println)
結果は、getメソッドで取り出します。このために、NoManageResourceクラスを作成しました…。それ以外のクラスでgetメソッドを呼び出すと、例外が飛んできます。
ちなみに、前のContinuationモナドの写経とは違い、for式が評価された段階でリソースはクローズされます。
foreachメソッドも実装しているので、普通に手続きっぽくも使えます。
for { fis <- new FileInputStream("src/main/scala/ResourceTest.scala").toR isr <- new InputStreamReader(fis, "UTF-8").toR reader <- new BufferedReader(isr).toR } { var line: String = null while ({ line = reader.readLine(); line != null }) { println(line) } }
implicit conversionはどこかに定義しないと利用できませんが、Resourceクラスのサブクラスを追加しても一応OKです。
例えば、こんなトレイト、クラスを用意しまして…
trait Releasable { def release(): Unit } class ReleasableResource[+A <: Releasable](r: A) extends Resource(r) { protected val disposable: Disposable = new Disposable { def dispose(): Unit = r.release() } } class Simple(val value: String) extends Releasable { def print(): Unit = println("Hello, " + value) def release(): Unit = println("Close[" + value + "]") }
あとは、for式に放り込みます。
Resource addResourceFactory { case r: Releasable => new ReleasableResource(r) } for { s <- new ReleasableResource(new Simple("World")) } s.print()
最初でReleableをResourceに変換するPartialFunctionを追加していますが、foreachの場合Resource#selectは呼ばれないので、あんまり関係ありませんね。
なかなかてこずりましたが、自分でmap、flatMap、foreachを使えるように最初から実装したのは初めてだったので、けっこう面白かったです。
あ、そういえば、withFilter書いてないや…。