Izumi Reflect, Heterogeneous Maps
and ZLayer
by
Boris V.Kuznetsov
Adam Fraser
Chisel Crew, July 2020
About this talk
More Info here


Boris V. Kuznetsov
Adam Fraser
Dependency injection problem
- Class constructors
- Trait mixin
- Cake pattern
- Implicit arguments
- Object composition
- Dependency Injection Frameworks (Guice, MacWire, etc)
Which are problems with all those?
- Need to include all dependencies in declaration
- Need to instantiate all dependencies before instantiation
- Doen't scale well for many deps
- Frameworks are complex to debug, have a learning curve and don't share dependencies inside well
ZIO Solution
ZIO[-R, +E, +A]
equivalent
R => Either[E,A]
ZIO is a functional data structure, which describes a computation that requires R on input, may fail with an error of type E (Throwable, String, Custom) or yield exactly one value of type A
More info here
Dependency injection with ZLayer
class Kafka {
def talk() = "kafka"
}
class Cassandra {
def talk() = "cassandra"
}
class Elastic {
def talk() = "elastic"
}
Dependency injection with ZLayer
object moduleA {
// service binding
type ModuleA = Has[ModuleA.Service]
// service declaration
object ModuleA {
trait Service {
def run(): UIO[String]
}
}
// service implementation
val live = ZLayer.fromService { kafka: Kafka =>
new Service {
override def run() = UIO(kafka.talk())
}
}
// Public accessor
def run(): URIO[ModuleA, String] = ZIO.accessM(_.get.run())
}Dependency injection with ZLayer
object moduleB {
// service binding
type ModuleB = Has[ModuleB.Service]
// service declaration
object ModuleB {
trait Service {
def run(): UIO[String]
}
}
// service implementation
val live = ZLayer.fromServices((modA: ModuleA.Service, cass: Cassandra) =>
new Service {
def run(): UIO[String] = modA.run().map(_ + "_" + cass.talk())
}
)
// Public accessor
def run(): URIO[ModuleB, String] = ZIO.accessM(_.get.run())
}Dependency injection with ZLayer
object BaseSpec extends DefaultRunnableSpec {
def spec =
suite("ZLayerSpec")(
testM("Module A test") {
val res = moduleA.run()
assertM(res)(equalTo("kafka"))
}.provideCustomLayer(liveLayerA),
testM("Module B test") {
val res = moduleB.run()
assertM(res)(equalTo("kafka_cassandra"))
}.provideCustomLayerShared(liveLayer)
)
val kafkaLayer = ZLayer.succeed(new Kafka {})
val cassLayer = ZLayer.succeed(new Cassandra {})
val liveLayerA = kafkaLayer >>> moduleA.live
val liveLayer = (liveLayerA ++ cassLayer) >>> moduleB.live
}ZLayer benefits
- Clean, established pattern
- No external dependencies
- No need to instantiate dependencies
- No need to import all declarations
- Builds and optimizes an instance graph under the hood
- Delivers instance sharing and avoids extra memory allocations
The magic of zio.Has
final class Has[A] private (
private val map: Map[LightTypeTag, scala.Any],
private var cache: Map[LightTypeTag, scala.Any] = Map()
) extends Serializable {...}- Built with type tagging
- Uses izumi.reflect library
More on Izumi here
Type tagging
// defines a phantom type
// which exists compile time only
// No runtime information
trait A
// defines Int.
// Known both at compile time and runtime
// Maps to JVM primitive
val number = 4
// object of type A
// requires allocation since is not mapped on JVM primitive
val aVale = new A {}
// Defines a value of a type, which is
// Int with A at compile time
// Int in run time
val extNumber = number.asInstanceOf[Int with A]Type Tagging with Shapeless
import shapeless.labelled.{KeyTag, FieldType}
import shapeless.syntax.singleton._
// Int
val number = 11
// Enrich the Int type with a string type with signature "positiveNumber"
val numberAndString = "positiveNumber" ->> number
// numberAndString: Int with shapeless.labelled.KeyTag[String("positiveNumber"),Int] = 11
Heterogenous Map
Has[Blocking.Service] with
Has[Random.Service] with
Has[Clock.Service] with
Has[Console.Service] with
Has[System.Service]
Map(
Tagged[Blocking.Service] -> BlockingServiceLive,
Tagged[Random.Service] -> RandomServiceLive,
Tagged[Clock.Service] -> ClockServiceLive,
Tagged[Console.Service] -> ConsoleServiceLive,
Tagged[System.Service] -> SystemServiceLive
)
Izumi reflect
- Macro-based TypeTag implementation
- scala-reflect free
- Works on ScalaJS and Scala Native
- Preliminary support for Scala 3 (compiler issues need to be fixed)
- Supports =:= and <:<
- Can combine tags in run-time
- Can generate tags for unapplied tag constructors on Scala 2 (F[_])
- Provides a solid base for ZLayer
More info here
Izumi Reflect
trait Super
trait Child extends Super
assert(
Tag[Either[RuntimeException, Child]].tag <:<
Tag[Either[Throwable, Super]].tag
)
assert(
Tag[Either[RuntimeException, Child]].tag =:=
TagKK[Either].tag.combine(Tag[RuntimeException].tag, Tag[Child].tag)
)
assert(
Tag[Either[RuntimeException, Child]].tag <:<
TagKK[Either].tag.combine(Tag[Throwable].tag, Tag[Super].tag)
)Izumi Reflect
- Type system model and typer simulator are imprecise
- ... but "good enough" for the vast majority of real-world usecases
- Type boundaries support is limited
- F-bounded types are not preserved
- Existential types (forSome) are not supported
- Path-Dependent Types support is limited
- Scala 3 support is WIP
Caveats
Thank you!
Dependency Injection
By ourcrew
Dependency Injection
- 115