Basics

Quick Start

Suppose we have an abstract Greeter component and some other components that depend on it:

import distage.{ModuleDef, Injector, GCMode}

trait Greeter {
  def hello(name: String): Unit
}

final class PrintGreeter extends Greeter {
  override def hello(name: String) = println(s"Hello $name!") 
}

trait Byer {
  def bye(name: String): Unit
}

final class PrintByer extends Byer {  
  override def bye(name: String) = println(s"Bye $name!")
}

final class HelloByeApp(greeter: Greeter, byer: Byer) {
  def run(): Unit = {
    println("What's your name?")
    val name = readLine()
    
    greeter.hello(name)
    byer.bye(name)
  }
}

To actually run the HelloByeApp, we have to wire implementations of Greeter and Byer into it. We will not do it directly. First we’ll only declare the component interfaces we have and the implementations we want for them:

val HelloByeModule = new ModuleDef {
  make[Greeter].from[PrintGreeter]
  make[Byer].from[PrintByer]
  make[HelloByeApp] // `.from` is not required for concrete classes 
}
// HelloByeModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Byer}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintByer)) ((basics.md:67)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.HelloByeApp}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.Greeter, repl.Session::repl.Session.App0::repl.Session.App0.Byer): repl.Session::repl.Session.App0::repl.Session.App0.HelloByeApp)) ((basics.md:68)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Greeter}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintGreeter)) ((basics.md:66)))

ModuleDef merely contains a description of the desired object graph, let’s transform that high-level description into an actionable series of steps - an OrderedPlan, a datatype we can inspect, test or verify at compile-time ? without actually creating any objects or executing any effects.

val plan = Injector().plan(HelloByeModule, GCMode.NoGC)
// plan: izumi.distage.model.plan.OrderedPlan = {type.Session::App0::Byer} (basics.md:67) := call(<function1>(): Session::App0::PrintByer) {}
// {type.Session::App0::Greeter} (basics.md:66) := call(<function1>(): Session::App0::PrintGreeter) {}
// {type.Session::App0::HelloByeApp} (basics.md:68) := call(<function1>(Session::App0::Greeter, Session::App0::Byer): Session::App0::HelloByeApp) {
//   arg greeter: Session::App0::Greeter = lookup({type.Session::App0::Greeter})
//   arg byer: Session::App0::Byer = lookup({type.Session::App0::Byer})
// }

The series of steps must be executed to produce the object graph. Injector.produce will interpret the steps into a Resource value, that holds the lifecycle of the object graph:

// Interpret into DIResource

val resource = Injector().produce(plan)
// resource: izumi.distage.model.definition.DIResource.DIResourceBase[izumi.fundamentals.platform.functional.package.Identity, izumi.distage.model.Locator] = izumi.distage.model.definition.DIResource$$anon$10@2fa0a78

// Use the object graph:
// After `.use` exits, all objects will be deallocated,
// and all allocated resources will be freed.

resource.use {
  objects =>
    objects.get[HelloByeApp].run()
}
// What's your name?
// > izumi
// Hello izumi!
// Bye izumi!
// res1: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

distage always creates components exactly once, even if multiple other objects depend on them. There is only a “Singleton” scope. It’s impossible to create non-singletons in distage. If you need multiple singleton instances of the same type, you can create named instances and disambiguate between them using @Id annotation.

import distage.Id

new ModuleDef {
  make[Byer].named("byer-1").from[PrintByer]
  make[Byer].named("byer-2").from {
    otherByer: Byer @Id("byer-1") =>
      new Byer {
        def bye(name: String) = otherByer.bye(s"NOT-$name")
      }
  }
}
// res2: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Byer@byer-1}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintByer)) ((basics.md:97)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Byer@byer-2}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.Byer): {java.lang.Object & repl.Session::repl.Session.App0::repl.Session.App0.Byer})) ((basics.md:98)))

For true non-singleton semantics, you must create explicit factory classes or generate them (see Auto-Factories)

Activation Axis

You can choose between different implementations of a component using Axis tags:

import distage.{Axis, Activation, ModuleDef, Injector, GCMode}

class AllCapsGreeter extends Greeter {
  def hello(name: String) = println(s"HELLO ${name.toUpperCase}")
}

// declare the configuration axis for our components

object Style extends Axis {
  case object AllCaps extends AxisValueDef
  case object Normal extends AxisValueDef
}

// Declare a module with several implementations of Greeter
// but in different environments

val TwoImplsModule = new ModuleDef {
  make[Greeter].tagged(Style.Normal)
    .from[PrintGreeter]
  
  make[Greeter].tagged(Style.AllCaps)
    .from[AllCapsGreeter]
}
// TwoImplsModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Greeter}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.AllCapsGreeter)).tagged(Set(AxisTag(style:allcaps))) ((basics.md:128)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Greeter}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintGreeter)).tagged(Set(AxisTag(style:normal))) ((basics.md:125)))

// Combine previous `HelloByeModule` with our new module
// While overriding `make[Greeter]` bindings from the first module 

val CombinedModule = HelloByeModule overridenBy TwoImplsModule
// CombinedModule: izumi.distage.model.definition.Module = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Byer}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintByer)) ((basics.md:67)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.HelloByeApp}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.Greeter, repl.Session::repl.Session.App0::repl.Session.App0.Byer): repl.Session::repl.Session.App0::repl.Session.App0.HelloByeApp)) ((basics.md:68)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Greeter}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.AllCapsGreeter)).tagged(Set(AxisTag(style:allcaps))) ((basics.md:128)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Greeter}].from(call(<function1>(): repl.Session::repl.Session.App0::repl.Session.App0.PrintGreeter)).tagged(Set(AxisTag(style:normal))) ((basics.md:125)))

// Choose component configuration when making an Injector:

val capsInjector = Injector(Activation(Style -> Style.AllCaps))
// capsInjector: Injector = izumi.distage.InjectorDefaultImpl@33bfab9e

// Check the result:

capsInjector
  .produce(CombinedModule, GCMode.NoGC)
  .use(_.get[HelloByeApp].run())
// What's your name?
// > kai
// HELLO KAI
// Bye kai!
// res3: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

// Check that result changes with a different configuration:

Injector(Activation(Style -> Style.Normal))
  .produce(CombinedModule, GCMode.NoGC)
  .use(_.get[HelloByeApp].run())
// What's your name?
// > Pavel
// Hello Pavel!
// Bye Pavel!
// res4: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

In distage.StandardAxis there are three example Axes for back-end development: Repo.Prod/Dummy, Env.Prod/Test & ExternalApi.Prod/Mock

In distage-framework’s RoleAppLauncher, you can choose axes using the -u command-line parameter:

./launcher -u repo:dummy app1

In distage-testkit, specify axes via TestConfig:

import distage.StandardAxis.Repo
import izumi.distage.testkit.TestConfig
import izumi.distage.testkit.scalatest.DistageBIOSpecScalatest

class AxisTest extends DistageBIOSpecScalatest[zio.IO] {
  override protected def config: TestConfig = TestConfig(
    // choose implementations tagged `Repo.Dummy` when multiple implementations with `Repo.*` tags are available
    activation = Activation(Repo -> Repo.Dummy)
  )
}

Resource Bindings, Lifecycle

You can specify objects’ lifecycle by injecting cats.effect.Resource, zio.ZManaged or distage.DIResource values that specify the allocation and finalization actions for an object.

Injector itself only returns a DIResource value that can be used to create and finalize the object graph, this value is pure and can be reused multiple times. A DIResource is consumed using its .use method, the function passed to use will receive an allocated resource and when the function exits the resource will be deallocated.

Example with cats.effect.Resource:

import distage.{GCMode, ModuleDef, Injector}
import cats.effect.{Bracket, Resource, IO}

class DBConnection
class MessageQueueConnection

val dbResource = Resource.make(
  acquire = IO { 
    println("Connecting to DB!")
    new DBConnection 
})(release = _ => IO(println("Disconnecting DB")))
// dbResource: Resource[IO, DBConnection] = Allocate(<function1>)

val mqResource = Resource.make(
  acquire = IO {
   println("Connecting to Message Queue!")
   new MessageQueueConnection 
})(release = _ => IO(println("Disconnecting Message Queue")))
// mqResource: Resource[IO, MessageQueueConnection] = Allocate(<function1>)

class MyApp(db: DBConnection, mq: MessageQueueConnection) {
  val run = IO(println("Hello World!"))
}

val module = new ModuleDef {
  make[DBConnection].fromResource(dbResource)
  make[MessageQueueConnection].fromResource(mqResource)
  addImplicit[Bracket[IO, Throwable]]
  make[MyApp]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App5::repl.Session.App5.MyApp}].from(call(<function1>(repl.Session::repl.Session.App5::repl.Session.App5.DBConnection, repl.Session::repl.Session.App5::repl.Session.App5.MessageQueueConnection): repl.Session::repl.Session.App5::repl.Session.App5.MyApp)) ((basics.md:210)), make[{type.cats.effect.Bracket[=? %1:0 ? IO[+1:0],=Throwable]}].from(value(cats.effect.IOLowPriorityInstances$IOEffect@19126f75)) ((basics.md:209)), make[{type.repl.Session::repl.Session.App5::repl.Session.App5.MessageQueueConnection}].from(allocate[? %0 ? cats.effect.IO[+0]](call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@1ac6eae4(cats.effect.Bracket[=? %0 ? IO[+0],=Throwable]): izumi.distage.model.definition.DIResource::izumi.distage.model.definition.DIResource.Cats[=? %1:0 ? IO[+1:0],=Session::App5::MessageQueueConnection]))) ((basics.md:208)), make[{type.repl.Session::repl.Session.App5::repl.Session.App5.DBConnection}].from(allocate[? %0 ? cats.effect.IO[+0]](call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@3d6436bf(cats.effect.Bracket[=? %0 ? IO[+0],=Throwable]): izumi.distage.model.definition.DIResource::izumi.distage.model.definition.DIResource.Cats[=? %1:0 ? IO[+1:0],=Session::App5::DBConnection]))) ((basics.md:207)))

Will produce the following output:

val objectGraphResource = Injector().produceF[IO](module, GCMode.NoGC)
// objectGraphResource: izumi.distage.model.definition.DIResource.DIResourceBase[IO, izumi.distage.model.Locator] = izumi.distage.model.definition.DIResource$$anon$10@115cba06

objectGraphResource
  .use(_.get[MyApp].run)
  .unsafeRunSync()
// Connecting to Message Queue!
// Connecting to DB!
// Hello World!
// Disconnecting DB
// Disconnecting Message Queue

Lifecycle management DIResource is also available without an effect type, via DIResource.Simple and DIResource.Mutable:

import distage.{DIResource, GCMode, ModuleDef, Injector}

class Init {
  var initialized = false
}

class InitResource extends DIResource.Simple[Init] {
  override def acquire = {
    val init = new Init
    init.initialized = true
    init
  }
  override def release(init: Init) = {
    init.initialized = false
  }
}

val module = new ModuleDef {
  make[Init].fromResource[InitResource]
}
// module: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App7::repl.Session.App7.Init}].from(allocate[? %0 ? 0](call(<function1>(): repl.Session::repl.Session.App7::repl.Session.App7.InitResource))) ((basics.md:252)))

val closedInit = Injector().produce(module, GCMode.NoGC).use {
  objects =>
    val init = objects.get[Init] 
    println(init.initialized)
    init
}
// true
// closedInit: izumi.fundamentals.platform.functional.package.Identity[Init] = repl.Session$App7$Init@360afdfc

println(closedInit.initialized)
// false

DIResource forms a monad and has the expected .map, .flatMap, .evalMap, .mapK methods.

You can convert between DIResource and cats.effect.Resource via .toCats/.fromCats methods, and between zio.ZManaged via .toZIO/.fromZIO.

You need to use resource-aware Injector.produce/Injector.produceF, instead of produceUnsafe to be able to deallocate the object graph.

Set Bindings

Set bindings are useful for implementing listeners, plugins, hooks, http routes, healthchecks, migrations, etc. Everywhere where a collection of components is required, a Set Binding is appropriate.

To define a Set binding use .many and .add methods of the ModuleDef DSL.

For example, we may declare many http4s routes and serve them all from a central router:

// import boilerplate
import cats.implicits._
import cats.effect.{Bracket, IO, Resource}
import distage.{GCMode, ModuleDef, Injector}
import org.http4s._
import org.http4s.server.Server
import org.http4s.client.Client
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.client.blaze.BlazeClientBuilder

import scala.concurrent.ExecutionContext.Implicits.global

implicit val contextShift = IO.contextShift(global)
implicit val timer = IO.timer(global)
val homeRoute = HttpRoutes.of[IO] { 
  case GET -> Root / "home" => Ok(s"Home page!") 
}
// homeRoute: HttpRoutes[IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$17084/0x0000000844067040@5ba1cadb)

object HomeRouteModule extends ModuleDef {
  many[HttpRoutes[IO]]
    .add(homeRoute)
}

We’ve used many method to declare an open Set of http routes and then added one HTTP route into it. When module definitions are combined, Sets for the same binding will be merged together. You can summon a Set Bindings by summoning a scala Set, as in Set[HttpRoutes[IO]].

Let’s define a new module with another route:

val blogRoute = HttpRoutes.of[IO] { 
  case GET -> Root / "blog" / post => Ok(s"Blog post ``$post''!") 
}
// blogRoute: HttpRoutes[IO] = Kleisli(org.http4s.HttpRoutes$$$Lambda$17084/0x0000000844067040@3c522ecb)

object BlogRouteModule extends ModuleDef {  
  many[HttpRoutes[IO]]
    .add(blogRoute)
}

Now it’s the time to define a Server component to serve all the different routes we have:

def makeHttp4sServer(routes: Set[HttpRoutes[IO]]): Resource[IO, Server[IO]] = {
  // create a top-level router by combining all the routes
  val router: HttpApp[IO] = routes.toList.foldK.orNotFound

  // return a Resource value that will setup an http4s server 
  BlazeServerBuilder[IO]
    .bindHttp(8080, "localhost")
    .withHttpApp(router)
    .resource
}

object HttpServerModule extends ModuleDef {
  make[Server[IO]].fromResource(makeHttp4sServer _)
  make[Client[IO]].fromResource(BlazeClientBuilder[IO](global).resource)
  addImplicit[Bracket[IO, Throwable]] // required for cats `Resource` in `fromResource`
}

// join all the module definitions
val finalModule = Seq(
  HomeRouteModule,
  BlogRouteModule,
  HttpServerModule,
).merge
// finalModule: izumi.distage.model.definition.Module = Module(make[{type.org.http4s.server.Server[=? %1:0 ? IO[+1:0]]}].from(allocate[? %0 ? cats.effect.IO[+0]](call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@27f70bb3(scala.collection.immutable.Set[=Kleisli[=? %2:0 ? OptionT[=? %4:0 ? IO[+4:0],=2:0],-Request[=? %3:0 ? IO[+3:0]],=Response[=? %3:0 ? IO[+3:0]]]], cats.effect.Bracket[=? %0 ? IO[+0],=Throwable]): izumi.distage.model.definition.DIResource::izumi.distage.model.definition.DIResource.Cats[=? %1:0 ? IO[+1:0],=Server[=? %2:0 ? IO[+2:0]]]))) ((basics.md:356)), many[{type.scala.collection.immutable.Set[=Kleisli[=? %2:0 ? OptionT[=? %4:0 ? IO[+4:0],=2:0],-Request[=? %3:0 ? IO[+3:0]],=Response[=? %3:0 ? IO[+3:0]]]]}].add[{type.cats.data.Kleisli[=? %1:0 ? OptionT[=? %3:0 ? IO[+3:0],=1:0],-Request[=? %2:0 ? IO[+2:0]],=Response[=? %2:0 ? IO[+2:0]]]}].from(call(izumi.distage.model.providers.ProviderMagnet$$$Lambda$15554/0x0000000843a6f040@6235fbaf(): cats.data.Kleisli[=? %1:0 ? OptionT[=? %3:0 ? IO[+3:0],=1:0],-Request[=? %2:0 ? IO[+2:0]],=Response[=? %2:0 ? IO[+2:0]]])) ((basics.md:322)), many[{type.scala.collection.immutable.Set[=Kleisli[=? %2:0 ? OptionT[=? %4:0 ? IO[+4:0],=2:0],-Request[=? %3:0 ? IO[+3:0]],=Response[=? %3:0 ? IO[+3:0]]]]}] ((basics.md:321)), make[{type.cats.effect.Bracket[=? %1:0 ? IO[+1:0],=Throwable]}].from(value(cats.effect.IOInstances$$anon$2@79445518)) ((basics.md:358)), make[{type.org.http4s.client.Client[=? %1:0 ? IO[+1:0]]}].from(allocate[? %0 ? cats.effect.IO[+0]](call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@34e128a8(cats.effect.Bracket[=? %0 ? IO[+0],=Throwable]): izumi.distage.model.definition.DIResource::izumi.distage.model.definition.DIResource.Cats[=? %1:0 ? IO[+1:0],=Client[=? %2:0 ? IO[+2:0]]]))) ((basics.md:357)), many[{type.scala.collection.immutable.Set[=Kleisli[=? %2:0 ? OptionT[=? %4:0 ? IO[+4:0],=2:0],-Request[=? %3:0 ? IO[+3:0]],=Response[=? %3:0 ? IO[+3:0]]]]}].add[{type.cats.data.Kleisli[=? %1:0 ? OptionT[=? %3:0 ? IO[+3:0],=1:0],-Request[=? %2:0 ? IO[+2:0]],=Response[=? %2:0 ? IO[+2:0]]]}].from(call(izumi.distage.model.providers.ProviderMagnet$$$Lambda$15554/0x0000000843a6f040@6a0cfcf(): cats.data.Kleisli[=? %1:0 ? OptionT[=? %3:0 ? IO[+3:0],=1:0],-Request[=? %2:0 ? IO[+2:0]],=Response[=? %2:0 ? IO[+2:0]]])) ((basics.md:336)))

// wire the graph
val objects = Injector().produceUnsafeF[IO](finalModule, GCMode.NoGC).unsafeRunSync()
// objects: izumi.distage.model.Locator = izumi.distage.LocatorDefaultImpl@696c516c

val server = objects.get[Server[IO]]
// server: Server[IO] = BlazeServer(/127.0.0.1:8080)
val client = objects.get[Client[IO]]
// client: Client[IO] = org.http4s.client.Client$$anon$1@8e8a4a0

Check if it works:

// check home page
client.expect[String]("http://localhost:8080/home").unsafeRunSync()
// res10: String = Home page!

// check blog page
client.expect[String]("http://localhost:8080/blog/1").unsafeRunSync()
// res11: String = Blog post ``1''!

Further reading: the same concept is called Multibindings in Guice.

Effect Bindings

Sometimes we want to effectfully create a component, but the resulting component or data does not need to be deallocated. An example might be a global Semaphore to limit the parallelism of the entire application based on configuration, or a test implementation of some service made with Refs.

In these cases we can use .fromEffect to create a value using an effectful constructor.

Example with a Ref-based Tagless Final KVStore:

import distage.{GCMode, ModuleDef, Injector}
import izumi.functional.bio.{BIOMonadError, BIOPrimitives, F}
import zio.{Task, IO}

trait KVStore[F[_, _]] {
  def get(key: String): F[NoSuchElementException, String]
  def put(key: String, value: String): F[Nothing, Unit]
}

def dummyKVStore[F[+_, +_]: BIOMonadError: BIOPrimitives]: F[Nothing, KVStore[F]] = {
  for {
    ref <- F.mkRef(Map.empty[String, String])
  } yield new KVStore[F] {
    def put(key: String, value: String): F[Nothing, Unit] = {
      ref.update_(_ + (key -> value))
    }
  
    def get(key: String): F[NoSuchElementException, String] = {
      for {
        map <- ref.get
        res <- map.get(key) match {
          case Some(value) => F.pure(value)
          case None        => F.fail(new NoSuchElementException(key))
        }
      } yield res
    }
  }
}

val kvStoreModule = new ModuleDef {
  make[KVStore[IO]].fromEffect(dummyKVStore[IO])
}
// kvStoreModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App13::repl.Session.App13.KVStore[=? %1:0,%1:1 ? ZIO[-Any,+1:0,+1:1]]}].from(effect[? %0 ? zio.ZIO[-Any,+Nothing,+0]](value(zio.ZIO$FlatMap@4d2bc1c1))) ((basics.md:436)))

val io = Injector()
  .produceF[Task](kvStoreModule, GCMode.NoGC)
  .use {
    objects =>
      val kv = objects.get[KVStore[IO]]
      
      for {
        _    <- kv.put("apple", "pie")
        res1 <- kv.get("apple")
        _    <- kv.put("apple", "ipad")
        res2 <- kv.get("apple")
      } yield res1 + res2
  }
// io: Task[String] = zio.ZIO$CheckInterrupt@362fb754

new zio.DefaultRuntime{}.unsafeRun(io)
// res14: String = pieipad

You need to use effect-aware Injector.produceF/Injector.produceUnsafeF methods to use effect bindings.

Auto-Traits

distage can instantiate traits and structural types. All unimplemented fields in a trait or a refinement are filled in from the object graph.

This can be used to create ZIO Environment cakes with required dependencies - https://gitter.im/ZIO/Core?at=5dbb06a86570b076740f6db2

Trait implementations are derived at compile-time by TraitConstructor macro and can be summoned at need. Example:

import distage.{DIKey, GCMode, ModuleDef, Injector, ProviderMagnet, Tag}
import izumi.distage.constructors.TraitConstructor
import zio.console.{Console, putStrLn}
import zio.{UIO, URIO, URManaged, ZIO, Ref, Task}

trait Hello {
  def hello: UIO[String]
}
trait World {
  def world: UIO[String]
}

// Environment forwarders that allow
// using service functions from everywhere

val hello: URIO[{def hello: Hello}, String] = ZIO.accessM(_.hello.hello)
// hello: URIO[{def hello: App15.this.Hello}, String] = zio.ZIO$Read@542acb2c

val world: URIO[{def world: World}, String] = ZIO.accessM(_.world.world)
// world: URIO[{def world: App15.this.World}, String] = zio.ZIO$Read@734eada3

// service implementations

val makeHello = {
  (for {
    _     <- putStrLn("Creating Enterprise Hellower...")
    hello = new Hello { val hello = UIO("Hello") }
  } yield hello).toManaged { _ =>
    putStrLn("Shutting down Enterprise Hellower")
  }
}
// makeHello: zio.ZManaged[Console, Nothing, AnyRef with Hello{val hello: zio.UIO[String]}] = zio.ZManaged@686e5072

val makeWorld = {
  for {
    counter <- Ref.make(0)
  } yield new World {
    val world = counter.get.map(c => if (c < 1) "World" else "THE World")
  }
}
// makeWorld: ZIO[Any, Nothing, AnyRef with World{val world: zio.ZIO[Any,Nothing,String]}] = zio.ZIO$FlatMap@d77e324

// the main function

val turboFunctionalHelloWorld = {
  for {
    hello <- hello
    world <- world
    _     <- putStrLn(s"$hello $world")
  } yield ()
}
// turboFunctionalHelloWorld: ZIO[AnyRef with AnyRef with Console{def hello: App15.this.Hello;def world: App15.this.World}, Nothing, Unit] = zio.ZIO$FlatMap@76ba586e

// a generic function that creates an `R` trait where all fields are populated from the object graph

def provideCake[R: TraitConstructor, A: Tag](fn: R => A): ProviderMagnet[A] = {
  TraitConstructor[R].provider.map(fn)
}

val definition = new ModuleDef {
  make[Hello].fromResource(provideCake(makeHello.provide(_)))
  make[World].fromEffect(makeWorld)
  make[Console.Service[Any]].fromValue(Console.Live.console)
  make[UIO[Unit]].from(provideCake(turboFunctionalHelloWorld.provide))
}
// definition: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App15::repl.Session.App15.Hello}].from(allocate[? %0 ? zio.ZIO[-Any,+Nothing,+0]](call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@2b2a2df3(zio.console.Console::zio.console.Console.Service[=Any]): izumi.distage.model.definition.DIResource::izumi.distage.model.definition.DIResource.Zio[=Any,=Nothing,=({Object & Session::App15::Hello} & {def hello(): ZIO[-Any,+Nothing,+String]})]))) ((basics.md:526)), make[{type.zio.ZIO[-Any,+Nothing,+Unit]}].from(call(izumi.distage.model.reflection.universe.RuntimeDIUniverse.Provider$ProviderImpl$$Lambda$17045/0x000000084403e040@227cbd48(repl.Session::repl.Session.App15::repl.Session.App15.Hello, repl.Session::repl.Session.App15::repl.Session.App15.World, zio.console.Console::zio.console.Console.Service[=Any]): zio.ZIO[-Any,+Nothing,+Unit])) ((basics.md:529)), make[{type.zio.console.Console::zio.console.Console.Service[=Any]}].from(value(zio.console.Console$Live$$anon$1@21defb7a)) ((basics.md:528)), make[{type.repl.Session::repl.Session.App15::repl.Session.App15.World}].from(effect[? %0 ? zio.ZIO[-Any,+Nothing,+0]](value(zio.ZIO$FlatMap@d77e324))) ((basics.md:527)))

val main = Injector()
  .produceF[Task](definition, GCMode(DIKey.get[UIO[Unit]]))
  .use(_.get[UIO[Unit]])
// main: Task[Unit] = zio.ZIO$CheckInterrupt@73c944a2

new zio.DefaultRuntime{}.unsafeRun(main)
// Creating Enterprise Hellower...
// Hello World
// Shutting down Enterprise Hellower

Auto-Factories

distage can instantiate ‘factory’ classes from suitable traits. This feature is especially useful with Akka. All unimplemented methods with parameters in a trait will be filled by factory methods:

Given a class ActorFactory:

import distage._
import java.util.UUID

class SessionStorage

class UserActor(sessionId: UUID, sessionStorage: SessionStorage)

trait ActorFactory {
  // UserActor will be created as follows:
  //   sessionId argument is provided by the user
  //   sessionStorage argument is wired from the object graph
  def createActor(sessionId: UUID): UserActor
}

And a binding of ActorFactory without an implementation

class ActorModule extends ModuleDef {
  make[ActorFactory]
}

distage will derive and bind the following implementation for ActorFactory:

class ActorFactoryImpl(sessionStorage: SessionStorage) extends ActorFactory {
  override def createActor(sessionId: UUID): UserActor = {
    new UserActor(sessionId, sessionStorage)
  }
}

@With annotation can be used to specify the implementation class, when the factory result is abstract:

trait Actor { 
 def anyToUnit: Any => Unit = _ => ()
}

object Actor {
  trait Factory {
    def mkActor(name: String): Actor @With[Actor.Impl]
  }

  final class Impl(name: String) extends Actor{
    override def anyToUnit: Any => Unit = msg => println(s"Actor `$name` received a message: $msg")
  }
}

val factoryModule = new ModuleDef {
  make[Actor.Factory]
}
// factoryModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App15::repl.Session.App15.Actor::repl.Session.App15.Actor.Factory}].from(call(<function1>(): repl.Session::repl.Session.App15::repl.Session.App15.Actor::repl.Session.App15.Actor.Factory)) ((basics.md:602)))

Injector()
  .produce(factoryModule, GCMode.NoGC)
  .use(_.get[Actor.Factory].mkActor("Martin").anyToUnit("ping"))
// Actor `Martin` received a message: ping
// res17: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

You can use this feature to concisely provide non-Singleton semantics for some of your components.

Factory implementations are derived at compile-time by FactoryConstructor macro and can be summoned at need.

Tagless Final Style

Tagless Final is one of the popular patterns for structuring purely-functional applications. If you’re not familiar with tagless final you can skip this section.

Brief introduction to tagless final:

Advantages of distage as a driver for TF compared to implicits:

For example, let’s take freestyle’s tagless example and make it safer and more flexible by replacing dependencies on global imported implementations from with explicit modules.

First, the program we want to write:

import cats.Monad
import cats.effect.{ExitCode, Sync, IO}
import cats.syntax.all._
import distage.{GCMode, Module, ModuleDef, Injector, Tag, TagK, TagKK}

trait Validation[F[_]] {
  def minSize(s: String, n: Int): F[Boolean]
  def hasNumber(s: String): F[Boolean]
}
def Validation[F[_]: Validation]: Validation[F] = implicitly

trait Interaction[F[_]] {
  def tell(msg: String): F[Unit]
  def ask(prompt: String): F[String]
}
def Interaction[F[_]: Interaction]: Interaction[F] = implicitly

class TaglessProgram[F[_]: Monad: Validation: Interaction] {
  def program: F[Unit] = for {
    userInput <- Interaction[F].ask("Give me something with at least 3 chars and a number on it")
    valid     <- (Validation[F].minSize(userInput, 3), Validation[F].hasNumber(userInput)).mapN(_ && _)
    _         <- if (valid) Interaction[F].tell("awesomesauce!")
                 else       Interaction[F].tell(s"$userInput is not valid")
  } yield ()
}

def ProgramModule[F[_]: TagK: Monad]: Module = new ModuleDef {
  make[TaglessProgram[F]]
  addImplicit[Monad[F]]
}

TagK is distage’s analogue of TypeTag for higher-kinded types such as F[_], it allows preserving type-information at runtime for type parameters. You’ll need to add a TagK context bound to create a module parameterized by an abstract F[_]. To parameterize by non-higher-kinded types, use just Tag.

Now the interpreters for Validation and Interaction:

final class SyncValidation[F[_]](implicit F: Sync[F]) extends Validation[F] {
  def minSize(s: String, n: Int): F[Boolean] = F.delay(s.size >= n)
  def hasNumber(s: String): F[Boolean]       = F.delay(s.exists(c => "0123456789".contains(c)))
}
  
final class SyncInteraction[F[_]](implicit F: Sync[F]) extends Interaction[F] {
  def tell(s: String): F[Unit]  = F.delay(println(s))
  def ask(s: String): F[String] = F.delay("This could have been user input 1")
}

def SyncInterpreters[F[_]: TagK: Sync] = {
  new ModuleDef {
    make[Validation[F]].from[SyncValidation[F]]
    make[Interaction[F]].from[SyncInteraction[F]]
    addImplicit[Sync[F]]
  }
}

// combine all modules

def SyncProgram[F[_]: TagK: Sync] = ProgramModule[F] ++ SyncInterpreters[F]

// create object graph Resource

val objectsResource = Injector().produceF[IO](SyncProgram[IO], GCMode.NoGC)
// objectsResource: izumi.distage.model.definition.DIResource.DIResourceBase[IO, izumi.distage.model.Locator] = izumi.distage.model.definition.DIResource$$anon$10@3315b065

// run

objectsResource.use(_.get[TaglessProgram[IO]].program).unsafeRunSync()
// awesomesauce!

The program module is polymorphic over effect type. It can be instantiated by a different effect:

import zio.interop.catz._
import zio.Task

val ZIOProgram = ProgramModule[Task] ++ SyncInterpreters[Task]
// ZIOProgram: Module = Module(make[{type.cats.Monad[=? %0 ? ZIO[-Any,+Throwable,+0]]}].from(value(zio.interop.CatsConcurrent@25275e5a)) ((basics.md:659)), make[{type.repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %0 ? ZIO[-Any,+Throwable,+0]]}].from(call(<function1>(cats.effect.Sync[=? %0 ? ZIO[-Any,+Throwable,+0]]): repl.Session::repl.Session.App18::repl.Session.App18.SyncValidation[=? %0 ? ZIO[-Any,+Throwable,+0]])) ((basics.md:680)), make[{type.cats.effect.Sync[=? %0 ? ZIO[-Any,+Throwable,+0]]}].from(value(zio.interop.CatsConcurrent@25275e5a)) ((basics.md:682)), make[{type.repl.Session::repl.Session.App18::repl.Session.App18.TaglessProgram[=? %0 ? ZIO[-Any,+Throwable,+0]]}].from(call(<function1>(cats.Monad[=? %0 ? ZIO[-Any,+Throwable,+0]], repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %0 ? ZIO[-Any,+Throwable,+0]], repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %0 ? ZIO[-Any,+Throwable,+0]]): repl.Session::repl.Session.App18::repl.Session.App18.TaglessProgram[=? %0 ? ZIO[-Any,+Throwable,+0]])) ((basics.md:658)), make[{type.repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %0 ? ZIO[-Any,+Throwable,+0]]}].from(call(<function1>(cats.effect.Sync[=? %0 ? ZIO[-Any,+Throwable,+0]]): repl.Session::repl.Session.App18::repl.Session.App18.SyncInteraction[=? %0 ? ZIO[-Any,+Throwable,+0]])) ((basics.md:681)))

We may even choose different interpreters at runtime:

import zio.RIO
import zio.console.{Console, getStrLn, putStrLn}

object RealInteractionZIO extends Interaction[RIO[Console, ?]] {
  def tell(s: String): RIO[Console, Unit]  = putStrLn(s)
  def ask(s: String): RIO[Console, String] = putStrLn(s) *> getStrLn
}

val RealInterpretersZIO = {
  SyncInterpreters[RIO[Console, ?]] overridenBy new ModuleDef {
    make[Interaction[RIO[Console, ?]]].from(RealInteractionZIO)
  }
}
// RealInterpretersZIO: Module = Module(make[{type.repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %1:0 ? ZIO[-Console,+Throwable,+1:0]]}].from(call(izumi.distage.model.providers.ProviderMagnet$$$Lambda$15554/0x0000000843a6f040@485c0e7e(): repl.Session.App18.RealInteractionZIO)) ((basics.md:725)), make[{type.cats.effect.Sync[=? %0 ? ZIO[-Console,+Throwable,+0]]}].from(value(zio.interop.CatsConcurrent@25275e5a)) ((basics.md:682)), make[{type.repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %0 ? ZIO[-Console,+Throwable,+0]]}].from(call(<function1>(cats.effect.Sync[=? %0 ? ZIO[-Console,+Throwable,+0]]): repl.Session::repl.Session.App18::repl.Session.App18.SyncValidation[=? %0 ? ZIO[-Console,+Throwable,+0]])) ((basics.md:680)))

def chooseInterpreters(isDummy: Boolean) = {
  val interpreters = if (isDummy) SyncInterpreters[RIO[Console, ?]]
                     else         RealInterpretersZIO
  val module = ProgramModule[RIO[Console, ?]] ++ interpreters
  Injector().produceGetF[RIO[Console, ?], TaglessProgram[RIO[Console, ?]]](module)
}

// execute

chooseInterpreters(true)
// res20: izumi.distage.model.definition.DIResource.DIResourceBase[zio.ZIO[Console, Throwable, ?$5$], TaglessProgram[zio.ZIO[Console, Throwable, ?$6$]]] = izumi.distage.model.definition.DIResource$$anon$9@7214990

Modules can be polymorphic over arbitrary kinds - use TagKK to abstract over bifunctors:

class BifunctorIOModule[F[_, _]: TagKK] extends ModuleDef

Or use Tag.auto.T to abstract over any kind:

class MonadTransModule[F[_[_], _]: Tag.auto.T] extends ModuleDef
class TrifunctorModule[F[_, _, _]: Tag.auto.T] extends ModuleDef
class EldritchModule[F[+_, -_[_, _], _[_[_, _], _], _]: Tag.auto.T] extends ModuleDef

consult HKTag docs for more details.

Cats & ZIO Integration

Cats & ZIO instances and syntax are available automatically without imports, if cats-core, cats-effect or zio are already dependencies of your project. (Note: distage won’t bring cats or zio as a dependency if you don’t already use them. See No More Orphans for description of the technique)

Cats Resource Bindings will also work out of the box without any magic imports.

Example:

import cats.effect.IOApp
import distage.DIKey

trait AppEntrypoint {
  def run: IO[Unit]
}

object Main extends IOApp {
  def run(args: List[String]): IO[ExitCode] = {
    // ModuleDef has a Monoid instance
    val myModules = ProgramModule[IO] |+| SyncInterpreters[IO]
    val plan = Injector().plan(myModules, GCMode(DIKey.get[AppEntrypoint]))

    for {
      // resolveImportsF can effectfully add missing instances to an existing plan
      // (You can also create instances effectfully inside `ModuleDef` via `make[_].fromEffect` bindings)
      newPlan <- plan.resolveImportsF[IO] {
        case i if i.target == DIKey.get[DBConnection] =>
           DBConnection.create[IO]
      } 
      // `produceF` specifies an Effect to run in.
      // Effects used in Resource and Effect Bindings 
      // should match the effect in `produceF`
      _ <- Injector().produceF[IO](newPlan).use {
        classes =>
          classes.get[AppEntrypoint].run
      }
    } yield ExitCode.Success
  }
}