Loan Patternいろいろ(using, scalazのwithResource, scala-arm)

まずはコード1を見てください。

// コード1
def copyFile() = {
  val in = new FileInputStream(new File("foo.txt"))
  val out = new FileOutputStream(new File("bar.txt"))

  val buf = new Array[Byte](1024)
  var len = 0;
  while ({ len = in.read(buf); len != -1 }) {
    out.write(buf, 0, len)
  }

  in.close()
  out.close()
}

なんのへんてつもないファイルコピー(foo.txt -> bar.txt)のコードです。
別にこれでいいっちゃいいのですが、こういったコードの場合、closeを忘れたら残念なことが起こり得ます。

using

という訳でさんざん既出ですが、scalaではコード2のようにLoan Patternでcloseを保証する、ということををよくやります。
PythonのwithとかLispのwith-open-fileみたいなもんですね。

// コード2
def using[A, R <: { def close() }](r : R)(f : R => A) : A =
  try {
    f(r)
  } finally {
    r.close()
  }

def copyFileUsing() = {
  using(new FileInputStream(new File("foo.txt"))) { in =>
    using(new FileOutputStream(new File("bar.txt"))) {
      out => {
        val buf = new Array[Byte](1024)
        var len = 0;
        while ({ len = in.read(buf); len != -1 }) {
          out.write(buf, 0, len)
        }
      }
    }
  }
}

これで close し忘れることはなくなりました。やったね!


だがしかし、このLoan Pattern、簡単に作れるのは良いのですが、
ライブラリごとに似たりよったりなwithなんとかだのusingだのがあったりします。そろそろ見飽きました。
みんな作りすぎわろたの図

scalaz/withResource

自分でLoan Pattern作るのもそろそろ疲れました。
良さげなライブラリを探す旅に出ました。


scalazのwithResourceというものに出会いました。


scalaz/core/src/main/scala/scalaz/Resource.scala at master · scalaz/scalaz · GitHub

// コード3
object Resource {
  implicit val ResourceContravariant: Contravariant[Resource] = new Contravariant[Resource] {
    def contramap[A, B](r: Resource[A], f: B => A) = new Resource[B] {
      def close(b: B) = r close (f(b))
    }
  }

  implicit val InputStreamResource: Resource[InputStream] = new Resource[InputStream] {
    def close(c: InputStream) = c.close
  }

  implicit val OutputStreamResource: Resource[OutputStream] = new Resource[OutputStream] {
    def close(c: OutputStream) = c.close
  }


...(中略)...

trait Resources {
  def resource[T](cl: T => Unit): Resource[T] = new Resource[T] {
    def close(t: T) = cl(t)
  }

  def withResource[T, R](
                          value: => T
                        , evaluate: T => R
                        , whenComputing: Throwable => R = (t: Throwable) => throw t
                        , whenClosing: Throwable => Unit = _ => ()
                        )(implicit r: Resource[T]): R =
    try {
      val u = value
      try {
        evaluate(u)
      } finally {
        try {
          r close u
        } catch {
          case ex => whenClosing(ex)
        }
      }
    } catch {
      case ex => whenComputing(ex)
    }
}

これはコード4のようにして使います。

// コード4
def copyFileZ() = {
  import scalaz._
  import Scalaz._

  implicit val inResource: Resource[FileInputStream] = resource(fin => fin.close())
  implicit val outResource: Resource[FileOutputStream] = resource(fout => fout.close())

  withResource (
    new FileInputStream(new File("foo.txt")),
    (in: FileInputStream) => {
      withResource (
        new FileOutputStream(new File("bar.txt")),
        (out: FileOutputStream) => {
          val buf = new Array[Byte](1024)
          var len = 0;
          while ({ len = in.read(buf); len != -1 }) {
            out.write(buf, 0, len)
          }
        }
      )
    }
  )
}

そのままではimplicitな引数として、

  • InputStream
  • OutputStream
  • Connection
  • Statement
  • PreparedStatement
  • ResultSet

しかないのでそれ以外のものを扱いたい場合は、自分でResource[T]を定義してあげる必要があります。
そのためのメソッドがresourceです。


でも、ぶっちゃけあまりかっこよくはない、、
コード4のように入れ子になってしまうと特に読みづらい。。。

scala-arm

もうちょっと良さげなのが scala-arm です。


managedでリソースをManagedResourceというものに変換してくれます。
ManagedResourceにはmap/flatMap/foreachなどが定義されています。
従って、コード5のような使いかたができます。

// コード5
def copyFileARM() = {
  import resource._ // パッケージ微妙...

  for {
    in <- managed(new FileInputStream("foo.txt")) 
    out <- managed(new FileOutputStream("bar.txt")) 
  } {
    val buf = new Array[Byte](1024)
    var len = 0;
    while ({ len = in.read(buf); len != -1 }) {
      out.write(buf, 0, len)
    }
  }
}

これは良いですね!using/withResourceの入れ子も取れちゃいました。


forではなくacquireForというのを使うと、using/withResourceと見た目が近くなります。

// コード6
def copyFileARM2() = {
  import resource._

  managed(new FileInputStream("foo.txt")) acquireFor { in =>
    managed(new FileOutputStream("bar.txt")) acquireFor { out =>
      val buf = new Array[Byte](1024)
      var len = 0;
      while ({ len = in.read(buf); len != -1 }) {
        out.write(buf, 0, len)
      }
    }
  }
}

コード6ではもはや関係ないですが、acquireForはEither[List[Throwable], T]という型を返します。


次のコード7のように成功するとRightが返ります。

// コード7
// foo.txt を読んで一つの文字列にする
def fileToString() = {
  import resource._

  val sb = new StringBuilder
  managed(new FileReader("foo.txt")) map (new BufferedReader(_)) acquireFor { br =>
    var line: String = null
    while ({ line = br.readLine; line != null}) {
      sb.append(line + "\n")
    }
    sb.toString
  }
}
/*
実行結果
scala> fileToString
res0: Either[List[Throwable],java.lang.String] = 
Right(foo
bar
)
*/


コード8のように途中でExceptionが飛ぶとLeftが返ります。

// コード8
def acquireForAndException() = {
  import resource._

  managed(new FileReader("foo.txt")) acquireFor { r =>
    throw new IOException
  }
}
/*
実行結果
scala> acquireForAndException
res0: Either[List[Throwable],Nothing] = Left(List(java.io.IOException))
*/

まとめ

もうなんでもいいので標準にLoan Pattern欲しいです。

参考

↓scalaz withResource について


scala-armについて