CE3のIOLocalを試してみた

CE3で追加されたミュータブルなストレージであるIOLocalを試してみました。
2022.08.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

CE3にはIOLocalというクラスが追加されています。最近のBootzookaのリリースで採用されているようなことが書かれていたので気になって調べてみました。

概要

APIは以下のようになっています。

package cats.effect

sealed trait IOLocal[A] {

  def get: IO[A]

  def set(value: A): IO[Unit]

  def reset: IO[Unit]

  def update(f: A => A): IO[Unit]

  def modify[B](f: A => (A, B)): IO[B]

  def getAndSet(value: A): IO[A]

  def getAndReset: IO[A]

}

見てわかるとおりストレージとして動作して任意の値を保持、更新できます。ざっくりいうと以下のような挙動です。

  • 初期値を与えて生成して、値の変更、参照、リセットができる
  • 先に行った変更が(flatMapで合成した)後続のアクションにて行われる参照に反映される
  • spawnした後に行った変更は反映されない

spawnしたときの振る舞い

spawnしたIO(子)で行われる変更が親に伝搬しないのが特徴のようです。これを確かめてみます。

import cats.*
import cats.effect.*
import cats.implicits.*
object IOLocalExample extends IOApp.Simple {

  def putStrLn[A: Show](a: A): IO[Unit] = IO(
    println(s"${Thread.currentThread.getName}:  ${a.show}")
  )

  def printAndModify(storage: IOLocal[String])(newValue: String): IO[Unit] =
    IO.defer {
      for {
        _ <- storage.get >>= putStrLn
        _ <- storage.set(newValue)
        f <- storage.get
          .flatMap(a => storage.set(a + " child") *> storage.get >>= putStrLn)
          .start
        _ <- f.join
        _ <- storage.get >>= putStrLn // newValueのまま
      } yield ()
    }

  override def run: IO[Unit] = for {
    storage <- IOLocal("init") //初期値
    _ <- storage.get >>= putStrLn
    f1 <- printAndModify(storage)("f1").start //ここでspawnする
    _ <- storage.set("main") //値を変更する
    _ <- f1.join
    _ <- storage.get >>= putStrLn //ここでもmain
  } yield ()

}

/*
io-compute-4:  init
io-compute-8:  init
io-compute-6:  f1 child
io-compute-8:  f1
io-compute-8:  main
*/

まとめ

Bootzookaの記事でも言及されていましたがロギングコンテキストを持ち回るのに便利そうだなと思います。しかし他の不変なストレージとは違って思わぬ変更が行われる可能性があるので用途は限った方が安全かもしれません。