ZIO 2.0について

この記事はScala Advent Calendar 2021の17日目の記事です。

qiita.com

参考

この記事では下記のサイトから説明、コード、画像を引用しています。

ZIOとは

ZIOは純粋な関数型プログラミングを促進する非同期・並行プログラミング用ライブラリです。 モナドなどを知らなくても、ZIOを使うことで関数型プログラミングを始めることができます。

ZIO 2.0

つい先日ZIO2.0-RC1がリリースされました。 github.com

ZIO2.0では以下の4つの領域で大きな変更が加えられています。

  • Performance
  • Ergonomics
  • Operations
  • Streaming

ZIO2.0への移行ガイドはこちらになります。 zio.dev

以下では、上記の中からZIO 2.0での変更点で私が気になった点を取り上げました。

[Performance] 新しいスケジューラー

ZIO2.0ではスケジューラが新しくなりました。 ZIOではfiberという論理的なプロセスの単位で処理が実行されます。アプリケーションは無数のfibterから構成され、スケジューラーはfiberのOSスレッドへの割り当てを制御します。 ZIO 1.xではスケジューラーはグローバルな単一のキューとして実装されていました。

f:id:takatorix:20211216154206p:plain
ZIO1.xでのスケジューラーの実装。画像出典はhttps://ziverge.com/blog/advances-in-the-zio-2-scheduler/から

この実装の場合、全てのワーカースレッドが同じキューから取り出すため、多くのワーカースレッドが同時に操作しようとするとパフォーマンスに悪影響を及ぼす可能性がありました。また、グローバルキューを使用するとキャッシュの局所性を損ねパフォーマンスを向上させることができませんでした。

ZIO2.0では以下のようになりました。

f:id:takatorix:20211216165518p:plain
ZIO2.0でのスケジューラー 画像出典はhttps://ziverge.com/blog/advances-in-the-zio-2-scheduler/から

各ワーカースレッドごとにローカルのキューを持つことで、グローバルなリソースの競合を排除しキャッシュの局所性を最大化させることができます。しかしながら、それだけでは特定のワーカーにタスクが偏った場合リソースを効率的に使用できない可能性があります。そこで新しいスケジューラーはワーカーが自身のキューにタスクが存在しない場合に他のワーカーからタスクを盗むことができるようになっています。

詳細は以下のブログに記載されているのでご参照ください。 ziverge.com

ちなみにRustのTokioプロジェクトに触発されて開発したようです。

また、関連していそうなPRはこちらです。 github.com

[Ergonomics] Layerが使いやすくなった

fromService*がなくなりtoLayerに統一される

ZIO 1.xでは他のServiceに依存したServiceを作る場合、以下のように書かないといけませんでした。

val live: URLayer[Clock with Console, Logging] =
  ZLayer.fromServices[Clock.Service, Console.Service, Logging.Service] {
    (clock: Clock.Service, console: Console.Service) =>
      new Service {
        override def log(line: String): UIO[Unit] =
          for {
            current <- clock.currentDateTime.orDie
            _ <- console.putStrLn(current.toString + "--" + line).orDie
          } yield ()
      }
  }

ZIO 2.0ではtoLayerが導入されボイラープレートを書かなく済むようになります。

case class LoggingLive(console: Console, clock: Clock) extends Logging {
  override def log(line: String): UIO[Unit] =
    for {
      current <- clock.currentDateTime.orDie
      _       <- console.putStrLn(current.toString + "--" + line).orDie
    } yield ()
}

object LoggingLive {
  val layer: URLayer[Console with Clock, Logging] =
    (LoggingLive(_, _)).toLayer[Logging]
}

依存関係の構築が簡単に

ZIO 1.xでは複数のレイヤーを以下のように組み合わせてアプリケーション全体の依存グラフを構築する必要がありました。

val appLayer: URLayer[Any, DocRepo with UserRepo] =
  (((Console.live >>> Logging.live) ++ Database.live ++ (Console.live >>> Logging.live >>> BlobStorage.live)) >>> DocRepo.live) ++
    (((Console.live >>> Logging.live) ++ Database.live) >>> UserRepo.live)
    
val res: ZIO[Any, Nothing, Unit] = myApp.provide(appLayer)

依存関係が大きくなってくると上記をメンテするのが大変でした。また依存関係の宣言に間違いがあった場合、以下のようなメッセージがコンパイル時に出力されるのですが、わかりづらく解決するのが難しかったです。

type mismatch;
 found   : zio.URLayer[zio.Logging with zio.Database with zio.BlobStorage,zio.DocRepo]
    (which expands to)  zio.ZLayer[zio.Logging with zio.Database with zio.BlobStorage,Nothing,zio.DocRepo]
 required: zio.ZLayer[zio.Database with zio.BlobStorage,?,?]
    ((Database.live ++ BlobStorage.live) >>> DocRepo.live) ++

ZIO 2.0ではinjectを使用することで自動で依存関係が解決されるようになります!

val res: ZIO[Any, Nothing, Unit] =
  myApp.inject(
    Console.live,
    Logging.live,
    Database.live,
    BlobStorage.live,
    DocRepo.live,
    UserRepo.live
  )

Module Patternが変わる

ZIO1.xでのModule Patternは以下のようになっていました。

object logging {
  // Defining the service type by wrapping the service interface with Has[_] data type
  type Logging = Has[Logging.Service]

  // Companion object that holds service interface and its live implementation
  object Logging {
    trait Service {
      def log(line: String): UIO[Unit]
    }
    
    // Live implementation of the Logging service
    val live: ZLayer[Clock with Console, Nothing, Logging] =
      ZLayer.fromServices[Clock.Service, Console.Service, Logging.Service] {
        (clock: Clock.Service, console: Console.Service) =>
          new Logging.Service {
            override def log(line: String): UIO[Unit] =
              for {
                current <- clock.currentDateTime.orDie
                _       <- console.putStrLn(s"$current--$line")
              } yield ()
          }
      }
  }

  // Accessor Methods
  def log(line: => String): URIO[Logging, Unit] =
    ZIO.accessM(_.get.log(line))
}

ZIO 2.0 では Hasが削除され以下のようになります。

// Defining the Service Interface
trait Logging {
  def log(line: String): UIO[Unit]
}

// Accessor Methods Inside the Companion Object
object Logging {
  def log(line: String): URIO[Logging, Unit] =
    ZIO.serviceWithZIO(_.log(line))
}

// Implementation of the Service Interface
case class LoggingLive(console: Console, clock: Clock) extends Logging {
  override def log(line: String): UIO[Unit] =
    for {
      time <- clock.currentDateTime
      _    <- console.printLine(s"$time--$line").orDie
    } yield ()
}

// Converting the Service Implementation into the ZLayer
object LoggingLive {
  val layer: URLayer[Console with Clock, Logging] =
    (LoggingLive(_, _)).toLayer[Logging]
}

ZIO1.x系のmoduleパターンについては以前書きましたが、 Hasが削除された結果、覚えることが少なくなりよりシンプルに書けるようになっている気がします。

takatorix.hatenablog.com

[Ergonomics] Smart Constructorの導入

ZIO 1.xではZIOのコンストラクタとしてZIO.fromEitherZIO.fromOptionのような形で、元になるデータ型ごとにメソッドが用意されていました。

ZIO 2.0ではfromだけで様々なデータ型からZIOを生成できるようになっています。

ZIO.from {
  println("Entering example")
  for {
    result1 <- ZIO.from(Future.successful(1))
    result2 <- ZIO.from(Right(2))
    result3 <- ZIO.from(ZIO.from(3))
    result4 <- ZIO.from(Try(4))
  } yield result1 + result2 + result3 + result4
}

[Operations] Loggingがビルトインされるようになった

ZIO 2.0 では標準のlogging facadeが提供されるようになりました。 facadeなのでlogbacklog4jなどのloggingバックエンドはこれまで通り必要となります。

ZIO.logLevel(LogLevel.Warning) {
  ZIO.log("The response time exceeded its threshold!")
}

ZIO.logError("File does not exist: ~/var/www/favicon.ico")

今まではLogging Moduleを毎回自作していたので、その手間がなくなりそうです。

ziverge.com

まとめ

ZIO 2.0で導入される機能や変更の一部を紹介しました。様々な改善点が盛り込まれていてリリースされるのが待ち遠しいです。 今回は変更点が多すぎてほんの一部しか紹介できなかったので、ぜひマイグレーションガイドを読んでみてください。