Runner in the High

技術のことをかくこころみ

ZIOによるDIの最小サンプル

公式のドキュメントや他の人の書いたブログ記事を読みつつ書いてみたのでメモ。

zio.dev

import zio.{IO, RIO, Runtime, Task, UIO, URIO, ZIO}

// Userリポジトリのインタフェース

case class User(name: String)

object UserRepo {
  trait Service {
    def load(): Task[Option[User]]
    def store(user: User): Task[Unit]
  }

  def load: RIO[UserRepo.Service, Option[User]] =
    ZIO.accessM { env => env.load() }

  def store(user: User): RIO[UserRepo.Service, Unit] =
    ZIO.accessM { env => env.store(user) }
}

// ロガーのインタフェース

object Logger {
  trait Service {
    def info(msg: String): UIO[Unit]
    def error(msg: String): UIO[Unit]
  }

  def info(msg: String): URIO[Logger.Service, Unit] =
    ZIO.accessM { env => env.info(msg) }

  def error(msg: String): URIO[Logger.Service, Unit] =
    ZIO.accessM { env => env.error(msg) }
}

以上のインタフェースの実装を踏まえて、実装を用意する。

各実装は、上で完成したインターフェイスオブジェクト内部のServiceトレイトを実装している。

// オンメモリなUserリポジトリの実装(ただのモックだけど)

trait MemoryUserRepo extends UserRepo.Service {
  override def load(): Task[Option[User]] =
    IO.succeed(Some(User("seiya")))

  override def store(user: User): Task[Unit] =
    IO.succeed(())
}

// コンソール用ロガーの実装

trait ConsoleLogger extends Logger.Service {
  override def info(msg: String): UIO[Unit] =
    IO.succeed(println(s"[INFO] $msg"))

  override def error(msg: String): UIO[Unit] =
    IO.succeed(println(s"[ERROR] $msg"))
}

最後に依存する実装をミクスインしたDepsをアプリケーションに与えて起動する

object Deps extends MemoryUserRepo with ConsoleLogger

object ZIOApp extends App {
  val usecase =
    for {
      _ <- Logger.info("start")
      userM <- UserRepo.load
      _ <- userM.fold(Logger.error("no user"))(
        user => Logger.info(s"name: ${user.name}")
      )
      _ <- Logger.info("done")
    } yield ()

  Runtime.default.unsafeRun(usecase.provide(Deps))
}

2020年2月以降、ZIOには実装の依存関係を縦(Vertical)と横(Horizontal)の軸で合成できるようにするZLayerという機能が実装されたらしい。

自分も手元でZLayerを用いた実装を試してみようと思ったが、Scala力が足りないせいかコンパイルエラーが解消できなくて諦めた。

なので、このサンプル自体は動きはするがZIOのチカラが100%活かせているというわけではない。