Scalaの非同期処理フレームワークであるCats Effectには、非同期処理のためのバラエティ豊かな部品が沢山揃っていて、これらを組み合わせることで簡単に柔軟な非同期処理を書けるようになっている。
中でもよく登場する部品がResource
だ。Resource
を利用することで、以下のような処理を簡単に・柔軟にハンドリングできるようになる:
- 取得と解放を必ず行わなければならないような操作
- 例: ファイルへのロック
- ある処理のバックグラウンドで別の処理を行い、フォアグラウンド側の処理が終わったら終了させる
- 例: 処理中インジケータ
- 上記のような操作を合成した操作
- 例: 2つのファイルを順に開き、開いたときとは逆順に閉じなければならない
この記事ではResource
とその操作方法、どんなに便利かについて解説する。
環境
この記事ではScala 3.3.3、Cats Effect 3を利用する。
Scalaの実行環境としてScala CLIを利用する。
Resource
とは
Resource
(cats.effect.kernel.Resource
)とは、取得処理と解放処理のペアのことである(取得したリソースのことではない)。
Resource
は、取得と解放、入口と出口など、強制的に対にして扱う必要があるもののためにCats Effectが用意している部品である。Resource
を取得中にキャンセルが行われたりエラーが発生しても、Cats Effectは自動的に安全にResource
のシャットダウン処理を順に遂行してくれる。
Resource
の型シグネチャはResource[エフェクト型, 中身の型]
だ。例えばIO
の中でInt
をラップしたリソースの型はResource[IO, Int]
になる。
Cats Effectが標準で提供する部品のうち、以下のようなものがResource
として提供される:
Semaphore
やMutex
などロック系部品のlock
- ロックを取得し、あとで必ず解放する必要があるため
- ファイバを閉じることを保証する
Supervisor
- どの範囲を抜けたらファイバを閉じるか規定しなければならないため、その境界線を
Resource
として定める
- どの範囲を抜けたらファイバを閉じるか規定しなければならないため、その境界線を
Resource
は作ることができる
Resource
を作るにはいくつかの方法があるが、最も素朴なものはResource.make
だ。make
はリソースを作成する処理とそれを解放する処理とを引数に取り、Resource
を作成する:
import cats.effect.IO import cats.effect.kernel.Resource val r: Resource[IO, Int] = Resource.make(IO.println("Acquiring resource") >> IO(42))(n => IO.println(s"Releasing resource [$n]"))
Resource
はリソース概念の抽象化であり、まだこの時点では実際のリソース確保は行なわれない。
ちなみに、ただのIO
を解放処理なしでそのままResource
に持ち上げるResource.eval
も存在する。後述するがResource
同士は合成できるため、型を合わせるのに便利そうだ:
import cats.effect.IO import cats.effect.kernel.Resource val r: Resource[IO, Int] = Resource.eval(IO(42))
Resource
は使うことができる
Resource.use
を呼び出すと、そこで初めてリソースの確保処理が走り、ブロックの中ではそれが使えるようになる。リソースを確保する副作用があるため、use
の型はuse[B](f: A => IO[B]): IO[B]
であり、IO
を返す:
//> using scala 3.3.3 //> using dep "org.typelevel::cats-effect:3.5.4" //> using options "-Wnonunit-statement" import cats.effect.{IOApp, IO} import cats.effect.kernel.Resource object Main extends IOApp.Simple { val r = Resource.make(IO.println("Acquiring resource") >> IO(42))(_ => IO.println("Releasing resource") ) def run = for { _ <- r.use { n => // リソース確保処理が起動し、中身がもらえる IO.println(s"Using resource: $n") } // ここでリソースの利用区間は終わり。解放処理が走る } yield () }
実用的には、上掲のコードにあるように、リソース自体の定義は別に置いて、実際にそれが欲しいときにuse
を呼び出すのがわかりやすい。
また、Resource
自体はリソースの取得方法を表現しているだけなので、状態を持たないし、何度でも使っていい。
Resource
は合成できる
エンジニアの嬉しい嬉しいポイント、合成可能性だ。Resource
は他のResource
と合成して1つのResource
として扱うことができる。
例えば、次のような3つのResource
があるとしよう:
val mug = Resource.make(IO.println("Acquiring mug") >> IO("mug"))(_ => IO.println("Returning mug")) val pot = Resource.make(IO.println("Acquiring pot") >> IO("pot"))(_ => IO.println("Returning pot")) val teabag = Resource.make(IO.println("Acquiring teabag") >> IO("teabag"))(_ => IO.println("Returning teabag"))
おいしい紅茶を淹れるには、3つのリソースを全て確保しなければならない。
//> using scala 3.3.3 //> using dep "org.typelevel::cats-effect:3.5.4" //> using options "-Wnonunit-statement" import cats.effect.{IOApp, IO} import cats.effect.kernel.Resource object Main extends IOApp.Simple { val mug = Resource.make(IO.println("Acquiring mug") >> IO("mug"))(_ => IO.println("Returning mug")) val pot = Resource.make(IO.println("Acquiring pot") >> IO("pot"))(_ => IO.println("Returning pot")) val teabag = Resource.make(IO.println("Acquiring teabag") >> IO("teabag"))(_ => IO.println("Returning teabag")) def run = for { _ <- mug.use { m => pot.use { p => teabag.use { t => IO.println(s"Making tea with $m, $p, and $t 🍵") } } } } yield () }
これを実行すると以下のような結果になる(ちゃんと内側から解放されている!):
Acquiring mug Acquiring pot Acquiring teabag Making tea with mug, pot, and teabag 🍵 Returning teabag Returning pot Returning mug
さきほどの例では、use
して、use
して、use
してから使っている。こうする代わりに、先にResource
を合成してからuse
することで手数を減らせる。
Resource
は、for
や>>
、tupled
などで合成できる(flatMap
が実装されているため)。
//> using scala 3.3.3 //> using dep "org.typelevel::cats-effect:3.5.4" //> using options "-Wnonunit-statement" import cats.effect.{IOApp, IO} import cats.effect.kernel.Resource object Main extends IOApp.Simple { val mug = Resource.make(IO.println("Acquiring mug") >> IO("mug"))(_ => IO.println("Returning mug")) val pot = Resource.make(IO.println("Acquiring pot") >> IO("pot"))(_ => IO.println("Returning pot")) val teabag = Resource.make(IO.println("Acquiring teabag") >> IO("teabag"))(_ => IO.println("Returning teabag")) val triplet: Resource[IO, (String, String, String)] = for { m <- mug p <- pot t <- teabag } yield (m, p, t) def run = for { _ <- triplet.use { (m, p, t) => IO.println(s"Making tea with $m, $p, and $t 🍵") } } yield () }
上掲の例ではfor
式でResource
を1つのResource
に合成している。以下の例では同様のことをtupled
を使ってよりシンプルに表現している(リソース同士に依存関係がなければparTupled
を使うと同時並行でリソースを確保・解放する):
//> using scala 3.3.3 //> using dep "org.typelevel::cats-effect:3.5.4" //> using options "-Wnonunit-statement" import cats.effect.{IOApp, IO} import cats.effect.kernel.Resource // tupledを呼ぶために必要 import cats.syntax.all.{*, given} object Main extends IOApp.Simple { val mug = Resource.make(IO.println("Acquiring mug") >> IO("mug"))(_ => IO.println("Returning mug")) val pot = Resource.make(IO.println("Acquiring pot") >> IO("pot"))(_ => IO.println("Returning pot")) val teabag = Resource.make(IO.println("Acquiring teabag") >> IO("teabag"))(_ => IO.println("Returning teabag")) val triplet: Resource[IO, (String, String, String)] = (mug, pot, teabag).tupled def run = for { _ <- triplet.use { (m, p, t) => IO.println(s"Making tea with $m, $p, and $t 🍵") } } yield () }
まとめ
Resource
を使って取得・解放が必要な概念をうまく扱えることを示した。実際にはファイルを開いたり閉じたりする処理が入ってくるが、骨格となる部分は同じだ。Resource
があることで、「いつかちゃんと終了して返さなければならないリソース」の扱いに悩むことを減らせる。また、Resource
の合成可能性によって、非常に簡潔・柔軟にリソースハンドリングを行えることがわかった。