CLOVER🍀

That was when it all began.

Scala ARMを使ってみる

少し前にScalaz Effects IOを使ったプログラムを書いてみましたが、今回はScala ARMを使ってみたいと思います。

まあ、IO周りのライブラリを試してるだけですが、Scala ARMの場合はfor式を使ってリソースを扱うことができるので個人的には興味を引くのですが…。

Scala ARM
http://jsuereth.com/scala-arm/index.html

Scala Automatic Resource Management(GitHub
https://github.com/jsuereth/scala-arm

とりあえず、GitHubのページやBasic Usageなどを見て、使ってみたいと思います。

Basic Usage
http://jsuereth.com/scala-arm/usage.html

準備

依存関係の定義。Maven Central Repositoryに登録されているので、普通に設定すればいいです。
build.sbt

name := "scala-arm-example"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.10.3"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked")

libraryDependencies ++= Seq(
  "com.jsuereth" %% "scala-arm" % "1.3",
  "org.scalatest" %% "scalatest" % "2.0" % "test"
)

いくつかサンプルを

Scala ARMは、書き方として通常のものと限定継続を使ったものがあるのですが、今回は限定継続を使ったものは扱いません。

では、テストコードの雛形を。
src/test/scala/org/littlewings/arm/ScalaArmSpec.scala

package org.littlewings.arm

import java.io.StringWriter
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, NoSuchFileException, Paths}

import resource._

import org.scalatest.FunSpec
import org.scalatest.Matchers._

class ScalaArmSpec extends FunSpec {
  describe("scala-arm spec") {
    // ここに、テストコードを書く!!
  }
}

ちなみに、この1行がScala ARMを使うにあたり、必要なimport文です。

import resource._

まずは、最もシンプルなパターン。

    it("basic usage#1") {
      for (reader <- managed(Files.newBufferedReader(Paths.get("build.sbt"),
                                                     StandardCharsets.UTF_8))) {
        Iterator
          .continually(reader.readLine())
          .takeWhile(_ != null)
          .foreach(println)
      }
    }

managedメソッドにリソースを引数として与えることで、for式の中で使うことができます。また、for式(この場合foreach)を抜ける時に、リソースはクローズしてくれます。

Unitなものだけではなくて、for式でyieldを使用することで(というか、mapやflatMap)、ExtractableManagedResourceが返却されるので、ここから値を取得することができます。

    it("basic usage#2") {
      val managedResource =
        for (reader <- managed(Files.newBufferedReader(Paths.get("build.sbt"),
                                                       StandardCharsets.UTF_8)))
        yield
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .toList

      val lines = managedResource.acquireAndGet(identity)
      lines should have size 14
      lines.head should be ("name := \"scala-arm-example\"")
    }

値を取り出す時は、ExtractableManagedResource#acquireAndGetで。
この場合は、acquireAndGetを呼び出したタイミングでfor式の中身が実行され、終了時にリソースはクローズされます。

2つ以上、リソースを使っても大丈夫。

    it("basic usage#3") {
      val managedResource =
        for {
          reader <- managed(Files.newBufferedReader(Paths.get("build.sbt"),
                                                    StandardCharsets.UTF_8))
          writer <- managed(new StringWriter)
        } yield {
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .foreach { l =>
              writer.write(l)
              writer.write(System.lineSeparator)
            }

          writer.toString
        }

      val contents = managedResource.acquireAndGet(identity)
      contents should startWith ("name := \"scala-arm-example\"")
    }

ExtractableManagedResource#acquireAndGetの代わりに、ExtractableManagedResource#acquireForを使用すると結果が直接取得できるのではなくて、Eitherになります。

    it("basic usage#4") {
      val managedResource =
        for (reader <- managed(Files.newBufferedReader(Paths.get("build.sbt"),
                                                       StandardCharsets.UTF_8)))
        yield
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .toList

      val lines = managedResource.acquireFor(identity).right.get
      lines should have size 14
      lines.head should be ("name := \"scala-arm-example\"")
    }

続いて、例外発生時の挙動。

    it("throw exception#1") {
      an [NoSuchFileException] should be thrownBy {
        for (reader <- managed(Files.newBufferedReader(Paths.get("not-found"),
                                                       StandardCharsets.UTF_8))) {
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .foreach(println)
        }
      }
    }

foreachの場合は、その場で例外が飛んできます。リソースは、きちんとクローズされます。

ExtractableManagedResource#acquireAndGetの場合は、このメソッドを呼び出したタイミングで。

    it("throw exception#2") {
      val managedResource =
        for (reader <- managed(Files.newBufferedReader(Paths.get("not-found"),
                                                       StandardCharsets.UTF_8)))
        yield
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .toList

      an [NoSuchFileException] should be thrownBy managedResource.acquireAndGet(l => l)
    }

この場合も、やっぱりクローズはしてもらえるので大丈夫です。

ExtractableManagedResource#acquireForの場合はLeftになると思っていたのですが、なんか例外が飛んできましたね…。

    it("throw exception#3") {
      val managedResource =
        for (reader <- managed(Files.newBufferedReader(Paths.get("not-found"),
                                                       StandardCharsets.UTF_8)))
        yield
          Iterator
            .continually(reader.readLine())
            .takeWhile(_ != null)
            .toList

      an [NoSuchFileException] should be thrownBy managedResource.acquireFor(l => l)
    }

そんなに悪い感じはしないんですけど、実験的なライブラリの扱いなんでしょうかね?

また、リソースとして扱えるクラスですが、デフォルトではCloseableやConnection、closeメソッドを持ったものなどが組み込まれています。resource.ResourceコンパニオンオブジェクトのScaladocを見るとよいでしょう。

これで不足する場合は、自分でResourceトレイトをMix-inしたクラスを書けばよさそうですね。

Resource Typeclass
http://jsuereth.com/scala-arm/resource.html