CLOVER🍀

That was when it all began.

Scalaのfor式で扱えるクラスを書いてみる

ちょっと前に、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書いてないや…。