ZIO 2.0について
この記事はScala Advent Calendar 2021の17日目の記事です。
参考
この記事では下記のサイトから説明、コード、画像を引用しています。
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ではスケジューラーはグローバルな単一のキューとして実装されていました。
この実装の場合、全てのワーカースレッドが同じキューから取り出すため、多くのワーカースレッドが同時に操作しようとするとパフォーマンスに悪影響を及ぼす可能性がありました。また、グローバルキューを使用するとキャッシュの局所性を損ねパフォーマンスを向上させることができませんでした。
ZIO2.0では以下のようになりました。
各ワーカースレッドごとにローカルのキューを持つことで、グローバルなリソースの競合を排除しキャッシュの局所性を最大化させることができます。しかしながら、それだけでは特定のワーカーにタスクが偏った場合リソースを効率的に使用できない可能性があります。そこで新しいスケジューラーはワーカーが自身のキューにタスクが存在しない場合に他のワーカーからタスクを盗むことができるようになっています。
詳細は以下のブログに記載されているのでご参照ください。 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
が削除された結果、覚えることが少なくなりよりシンプルに書けるようになっている気がします。
[Ergonomics] Smart Constructorの導入
ZIO 1.xではZIOのコンストラクタとしてZIO.fromEither
やZIO.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なのでlogbackやlog4jなどの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を毎回自作していたので、その手間がなくなりそうです。
まとめ
ZIO 2.0で導入される機能や変更の一部を紹介しました。様々な改善点が盛り込まれていてリリースされるのが待ち遠しいです。 今回は変更点が多すぎてほんの一部しか紹介できなかったので、ぜひマイグレーションガイドを読んでみてください。