ZIO
最近、関数型Scala界隈で話題沸騰(?)のZIOを使用したリソース管理方法を紹介します。
ZIOについては、ZIO Environment 〜 Tagless Final の後継?で詳しく紹介されています。
ensuringメソッド
ensuringメソッドを利用することでfinally句と同様に確実に実行したい処理を記載することができます。
メソッドを呼び出した処理の成功・失敗に関わらず、ensuringメソッドの引数で記載された処理は実行されます。
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パターンです。
リソースの取得処理が失敗した場合は、解放処理は呼ばれません。
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クラスは合成可能で、複数の管理されたリソースを管理されたリソースのタプルとして扱うことができます。
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で副作用を遅延評価するようにしましょう。しないとダブルフリーなど意図しない動作になります。
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つの方法を紹介しましたが、リソース管理で通常使うのは、高レベルのbracketやZManagedになります。
ScalaでもResource acquisition is initialization (RAII) のコーディング・スタイルで、例外安全なコーディングが簡単にできそうです。
今回のコードはこちらです。
https://github.com/mitsutaka-takeda/zio_resource_management