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について
@xuwei_k: @tototoshi 怪しげ・・・というより限定継続返すメソッドの使い方よくわからなかったけど。でも、コード量かなり少ないし読みやすいし。最後に値取得するときに、例外にするのかEitherにするのか選べるなどちょっと柔軟だし、少なくともScalazのよりは設計考えられてると思う
2011-11-07 11:51:04 via web to @tototoshi