9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ZIOでのリソース管理

Last updated at Posted at 2019-04-11

ZIO

最近、関数型Scala界隈で話題沸騰(?)のZIOを使用したリソース管理方法を紹介します。

ZIOについては、ZIO Environment 〜 Tagless Final の後継?で詳しく紹介されています。

ensuringメソッド

ensuringメソッドを利用することでfinally句と同様に確実に実行したい処理を記載することができます。

メソッドを呼び出した処理の成功・失敗に関わらず、ensuringメソッドの引数で記載された処理は実行されます。

UseEnsuring.scala
import org.scalatest.WordSpecLike
import scalaz.zio._

class UseEnsuring extends WordSpecLike with DefaultRuntime {
  "ensuring" should {
    "release" in {
      unsafeRun(
        ZIO.effectTotal(println("acquire resource"))
          .ensuring(UIO.effectTotal(println("release resource")))
      )
    }
  }

  "ensuring" when {
    "failure happened" should {
      "release resource" in {
        assertThrows[FiberFailure](
          unsafeRun(
            (for {
              _ <- UIO.effectTotal(println("acquire resource"))
              _ <- IO.fail("Error!")
              _ <- UIO.effectTotal(println("Does not reach this line due to failure"))
            } yield ()).ensuring(
              UIO.effectTotal(println("release resource even after failure"))
            )))
      }
    }
  }
}

bracket

ensuringメソッドはfinal句のように最後に確実に実行したい処理を記述することができました。このメソッドは引数を受け取らないため、この中でリソースの解放処理を行うためには自身でリソース用の変数を用意するなどの準備が必要です。

その準備処理をまとめて提供しているのがbracketメソッドです。リソースの取得処理に対してこのメソッドを呼び出し、リソースの解放処理とリソースの使用処理を付与します。使用処理の終了時点で解放処理が必ず呼ばれます。Loanパターンです。

リソースの取得処理が失敗した場合は、解放処理は呼ばれません。

UseBracket.scala
import org.scalatest.WordSpecLike
import scalaz.zio.{DefaultRuntime, FiberFailure, UIO, ZIO}
import java.lang._

class MyResource1(var closed: Boolean = false) {
  def close(): Unit = {
    closed = true
  }

  def doSomething: Boolean = {
    require(!closed)
    true
  }
}

class ResourceFailedToInitialize() {
  throw new Exception("Initialization failure")

  def close(): Unit = {
    println("closed should not be called")
  }

  def doSomething(): Unit = ()
}

class UseBracket extends WordSpecLike with DefaultRuntime {

  "bracket" should {
    "release" in {
      unsafeRun(
        ZIO.effect(new MyResource1).bracket(r => UIO.effectTotal(r.close())) {
          resource =>
            UIO.effectTotal(resource.doSomething)
        }
      )
    }
  }

  "bracket" when {
    "initialization failed" should {
      "not call release" in {
        assertThrows[FiberFailure] {
          unsafeRun(
            ZIO.effect(new ResourceFailedToInitialize)
              .bracket(r => UIO.effectTotal(r.close())) {
                resource =>
                  UIO.effectTotal(resource.doSomething())
              }
          )
        }
      }
    }
  }
}

ZManagedクラス

管理されたリソース(取得と解放が関連づけられたリソース)を表現したクラスがZManagedです。

ZManagedクラスを利用すると、リソースの取得と解放の紐付けをデータとして扱うことができます。1箇所でリソースの取得と解放を定義して、複数箇所で使用する時に便利です。

またZManagedクラスは合成可能で、複数の管理されたリソースを管理されたリソースのタプルとして扱うことができます。

UseZManaged.scala
import org.scalatest.{Matchers, WordSpecLike}
import scalaz.zio._

class MyResource2(var closed: Boolean = false) {
  def close(): Unit = {
    closed = true
  }

  def doSomething: Boolean = {
    require(!closed)
    true
  }
}

class MyResource3(var closed: Boolean = false) {
  def close(): Unit = {
    closed = true
  }

  def doSomething: Boolean = {
    require(!closed)
    true
  }
}

class UseZManaged extends WordSpecLike with Matchers with DefaultRuntime {

  val myResource2IO: Task[ZManaged[Any, Nothing, MyResource2]] =
    ZIO.effect(
      ZManaged.make(ZIO.succeed(new MyResource2)) { m => UIO.effectTotal(m.close()) }
    )

  "Resource" should {
    "not outlive its scope" in {
      unsafeRun(
        for {
          myResource <- myResource2IO
          r <- myResource.use(
            r =>
              for {
                b <- ZIO.succeed(r.doSomething)
              } yield b
          )
        } yield r
      ) shouldBe true
    }
  }

  val multipleResourcesIO: Task[ZManaged[Any, Nothing, (MyResource3, MyResource2)]] = ZIO.effect(
    ZManaged.make(ZIO.succeed(new MyResource3)) { m => UIO.effectTotal(m.close())}.zipPar{
      ZManaged.make(ZIO.succeed(new MyResource2)) { m => UIO.effectTotal(m.close()) }
    }
  )

  "ZManaged" should {
    "combine multiple resources" in {
      unsafeRun(for {
        multipleResources <- multipleResourcesIO
        result <- multipleResources.use{
          case (r2, r3) =>
            UIO.succeed(r2.doSomething && r3.doSomething)
        }
      } yield result) shouldBe true
    }
  }
}

ZManaged使用時の注意点

ZManagedオブジェクトの生成ZManaged.makeは遅延評価ではないためIOで副作用を遅延評価するようにしましょう。しないとダブルフリーなど意図しない動作になります。

GotchaWithResourceManagement.scala
import org.scalatest.{Matchers, WordSpecLike}
import scalaz.zio._

class MyResource(var closed: Boolean) {
  def close(): Unit = {
    closed = true
  }

  def doSomething: Boolean = {
    require(!closed)
    true
  }
}

class GotchaWithResourceManagement extends WordSpecLike with Matchers with DefaultRuntime {

  val myResource: ZManaged[Any, Nothing, MyResource] = ZManaged.make(ZIO.succeed({
    new MyResource(false)
  })) { m => UIO.effectTotal(m.close()) }

  "ZManaged" should {
    "double free" in {
      assertThrows[FiberFailure](
        unsafeRun(
          for {
            result1 <- myResource.use(m => UIO.effectTotal(m.doSomething))
            result2 <- myResource.use(m => UIO.effectTotal(m.doSomething))
          } yield result1 && result2
        ))
    }
  }

  val myResourceIO: UIO[ZManaged[Any, Nothing, MyResource]] = ZIO.effectTotal(ZManaged.make(ZIO.succeed({
    new MyResource(false)
  })) { m => UIO.effectTotal(m.close()) })

  "ZManaged" should {
    "not double free" in {
      unsafeRun(
        for {
          resource1 <- myResourceIO
          resource2 <- myResourceIO
          result1 <- resource1.use(m => UIO.effectTotal(m.doSomething))
          result2 <- resource2.use(m => UIO.effectTotal(m.doSomething))
        } yield result1 && result2
      ) shouldBe true
    }
  }
}

まとめ

ZIOでリソース管理する方法を紹介しました。3つの方法を紹介しましたが、リソース管理で通常使うのは、高レベルのbracketZManagedになります。

ScalaでもResource acquisition is initialization (RAII) のコーディング・スタイルで、例外安全なコーディングが簡単にできそうです。

今回のコードはこちらです。
https://github.com/mitsutaka-takeda/zio_resource_management

9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?