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

More on this here

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