はじめに
ScalaのライブラリZIOにはDIを扱うための仕組みが組み込みで備わっている。
DIの実態をprovideする方法はいくつかあるので紹介する。
なおZIOの基本的な理解は所与とする。
DIを補助する仕組みとしてのZLayer
やHas
についてはこの記事がよくまとまっている。
provide
ZIO#provide
メソッドはZIO[R, _, _]
として依存しているR
をすべて解決する。
trait Logger { ... } trait DB { ... } val z: RIO[Logger with DB, Unit] = ... val env = new Logger with DB { ... } z.provide(env) // => ZIO[Any, Throwable, Unit]
依存はAny
となりどのコンポーネントにも依存しない実行可能なZIO
が生成される。
ちなみにすでに依存が存在しないprovideする必要のないZIO[Any, _, _]
に対してprovide
を呼ぶとコンパイルが通らない。
よくできている。
仕組みは簡単で、NeedsEnv[T]
というimplicit parameterをprovide
メソッドに付与し、Any
に対してのみわざと2つの同じ優先度を持つインスタンスを配置することで
implicit ambiguousを起こしている。
sealed trait ZIO[-R, +E, +A] extends Serializable with ZIOPlatformSpecific[R, E, A] { self => def provide(r: R)(implicit ev: NeedsEnv[R]): IO[E, A] = ... } sealed abstract class NeedsEnv[+R] object NeedsEnv extends NeedsEnv[Nothing] { // R =:= Any以外のときには問答無用でインスタンスを提供する implicit def needsEnv[R]: NeedsEnv[R] = NeedsEnv // R =:= Anyのときにはわざと2つインスタンスを配置しimplicit ambiguous implicit val needsEnvAmbiguous1: NeedsEnv[Any] = NeedsEnv implicit val needsEnvAmbiguous2: NeedsEnv[Any] = NeedsEnv }
provideSome
前述のprovide
メソッドでは依存R
を一気に解決する必要があったが、部分的に解決できたほうが便利なことも多い。provideSome
を使用することで部分的な解決が可能になる。
val z: RIO[Logger with DB, Unit] = ... z.provideSome[DB](db => new Logger with DB { // implementations using given DB instance } ) // => ZIO[DB, Throwable, Unit]
DB
を与えられたと仮定した場合のすべての依存の実装を提供することでDB
にのみ依存するZIO
が作られる。
注意しなければならないのが型パラメータでprovideSome[DB]
のように依存させるサービスを宣言しなければならない。
またprovide
のようにZIO[Any, _, _]
に対してprovideSome
を呼んでもコンパイルが通らない。
provideLayer
zioで依存を管理するときはZLayer
を使ったmoduleパターンで設計することが典型。provideLayer
を使用することでZLayer
をZIO
に依存として提供することができる。
val z: RIO[Has[Logger] with Has[DB], Unit] = ... val layer = ZLayer.succeedMany(Has.allOf(new Logger { ... }, new DB { ... })) z.provideLayer(layer) // => ZIO[Any, Throwable, Unit]
ただし一気にすべてのコンポーネントを提供しなければならない。
provideSomeLayer
provideSome
のように部分的にZLayer
の依存を解決できる。
val z: RIO[Has[Logger] with Has[DB], Unit] = ... val layer = ZLayer.succeed(new Logger { ... }) z.provideSomeLayer[Has[DB]](layer) // => ZIO[Has[DB], Throwable, Unit]
provideSome
の様に解決後も残ってしまう依存の型をパラメタで宣言しなければならない。
provideCustomLayer
provideSomeLayer
の特化版。
依存解決後も残ってしまう型がZEnv
に固定されたもの。zio.App
traitを使用したzioアプリケーションの実行時にはZEnv
の依存はデフォルト値が与えられるため、そのようなときに有用。
val z: RIO[Has[Logger] with Has[DB] with ZEnv, Unit] = ... val layer = ZLayer.succeedMany(Has.allOf(new Logger { ... }, new DB { ... })) z.provideCustomLayer(layer) // => ZIO[ZEnv, Throwable, Unit]
ZEnv
以外の依存はすべて与えなければならない。
おわり
zioきもちい。