Basics

Tutorial

Suppose we want to create an abstract Greeter component that we want to use without knowing its concrete implementation:

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

A simple implementation would be:

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

Let’s define some more components that depend on a Greeter:

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)
  }
}

The app above uses Greeter and Byer to hold a simple conversation with the user.

To actually run the app, we’ll have to bind the real implementations of Greeter and Byer.

import distage.{ModuleDef, Injector, GCMode}

object HelloByeModule extends ModuleDef {
  make[Greeter].from[PrintGreeter]
  make[Byer].from[PrintByer]
  make[HelloByeApp]
}

Since HelloByeApp is not an interface, but a final class that implements itself, we don’t have to specify an implementation class for it.

Let’s launch the app and see what happens:

val injector = Injector()

val plan = injector.plan(HelloByeModule, GCMode.NoGC)
val objects = injector.produceUnsafe(plan)

val app = objects.get[HelloByeApp]
app.run()
// What's your name?
// kai
// Hello kai!
// Bye kai!

Given a set of bindings, such as HelloByeModule, distage will lookup the dependencies (constructor or function arguments) of each implementation and deduce a plan to satisfy each dependency using the other implementations in that module. Once finished, it will happily return the plan back to you as a simple datatype. We can print HelloByeModule’s plan while we’re at it:

println(plan.render)
// {type.Session::App0::Byer} (basics.md:62) := make[Session::App0::PrintByer] ()
// {type.Session::App0::Greeter} (basics.md:61) := make[Session::App0::PrintGreeter] ()
// {type.Session::App0::HelloByeApp} (basics.md:63) := make[Session::App0::HelloByeApp] (
//   arg greeter: Session::App0::Greeter = lookup({type.Session::App0::Greeter})
//   arg byer: Session::App0::Byer = lookup({type.Session::App0::Byer})
// )

Since plan is just a piece of data, we need to interpret it to create the actual object graph ? Injector’s produce method is the default interpreter. Injector contains no logic of its own beyond interpreting instructions, its output is fully determined by the plan. This makes debugging quite easy.

Given that plans are data, it’s possible to verify them at compile-time or splice equivalent Scala code to do the instantiation before ever running the application. When used in that way, distage is a great alternative to compile-time frameworks such as MacWire all the while keeping the flexibility to interpret at runtime when needed. This flexibility in interpretation allows adding features, such as Plugins and Typesafe Config integration by transforming plans and bindings.

Note: classes in distage are always created exactly once, even if many different classes depend on them - they’re Singletons. Non-singleton semantics are not available, however you can create multiple named instances and disambiguate between them with @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")
      }
  }
}
// res3: AnyRef with ModuleDef = Set(SingletonBinding({type.repl.Session::repl.Session.App0::repl.Session.App0.Byer@byer-1},TypeImpl(repl.Session::repl.Session.App0::repl.Session.App0.PrintByer),Set(),(basics.md:117)), SingletonBinding({type.repl.Session::repl.Session.App0::repl.Session.App0.Byer@byer-2},ProviderImpl({java.lang.Object & repl.Session::repl.Session.App0::repl.Session.App0.Byer},<function1>(repl.Session::repl.Session.App0::repl.Session.App0.Byer): {java.lang.Object & repl.Session::repl.Session.App0::repl.Session.App0.Byer}),Set(),(basics.md:118)))

You can also create factory classes to help you mint new non-singleton instances. Auto-Factories can reduce boilerplate involved in doing this.

Modules can be combined into larger modules with ++ and overridenBy operators. Let’s use overridenBy to greet in ALL CAPS:

val caps = HelloByeModule.overridenBy(new ModuleDef {
  make[Greeter].from(new Greeter {
    override def hello(name: String) = println(s"HELLO ${name.toUpperCase}")
  })
})

val capsUniverse = injector.produceUnsafe(caps, GCMode.NoGC)
capsUniverse.get[HelloByeApp].run()
// What's your name?
// kai
// HELLO KAI
// Bye kai!

We’ve overriden the Greeter binding in HelloByeModule with an implementation of Greeter that prints in ALL CAPS. For simple cases like this we can write implementations right inside the module.

Function Bindings

To bind to a function instead of a class constructor use .from method in ModuleDef DSL:

import distage._

case class HostPort(host: String, port: Int)

class HttpServer(hostPort: HostPort)

object HttpServerModule extends ModuleDef {
  make[HttpServer].from {
    hostPort: HostPort =>
      val modifiedPort = hostPort.port + 1000
      new HttpServer(hostPort.copy(port = modifiedPort))
  }
}

To inject named instances or config values, add annotations such as @Id and @ConfPath to lambda arguments’ types:

import distage.config._

object HostPortModule extends ModuleDef {
  make[HostPort].named("default").from(HostPort("localhost", 8080))
  make[HostPort].from {
    (maybeConfigHostPort: Option[HostPort] @ConfPath("http"),
     defaultHostPort: HostPort @Id("default")) =>
      maybeConfigHostPort.getOrElse(defaultHostPort)
  }
}

Given a Locator we can retrieve instances by type, call methods on them or summon them with a function:

import scala.util.Random

val objects = Injector().produceUnsafe(HelloByeModule, GCMode.NoGC)
// objects: Locator = izumi.distage.LocatorDefaultImpl@1b2bb160

objects.run {
  (hello: Hello, bye: Bye) =>
    val names = Array("Snow", "Marisa", "Shelby")
    val rnd = Random.nextInt(3)
    println(s"Random index: $rnd")
    hello(names(rnd))
    bye(names(rnd))
}
// Random index: 1
// Hello Marisa
// Bye Marisa
objects.runOption { i: Int => i + 10 } match {
  case None => println("There is no Int in the object graph!")
  case Some(i) => println(s"Int is $i")
}
// There is no Int in the object graph!

consult ProviderMagnet docs for more details.

Set Bindings

Set bindings are useful for implementing event listeners, plugins, hooks, http routes, healthchecks, migrations, etc. Everywhere where you need to gather up a bunch of similar components is probably a good place for a Set Binding.

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

For example, we can gather and serve all the different http4s routes added in multiple independent modules:

// boilerplate
import cats.implicits._
import cats.effect._
import distage._
import org.http4s._
import org.http4s.Uri.uri
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.server.blaze._

import scala.concurrent.ExecutionContext.Implicits.global

implicit val contextShift = IO.contextShift(global)
implicit val timer = IO.timer(global)
object HomeRouteModule extends ModuleDef {

  val homeRoute = HttpRoutes.of[IO] { 
    case GET -> Root / "home" => Ok(s"Home page!") 
  }

  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:

object BlogRouteModule extends ModuleDef {

  val blogRoute = HttpRoutes.of[IO] { 
    case GET -> Root / "blog" / post => Ok(s"Blog post ``$post''!") 
  }
  
  many[HttpRoutes[IO]]
    .add(blogRoute)
}

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

final class HttpServer(routes: Set[HttpRoutes[IO]]) {
  
  val router: HttpApp[IO] = 
    routes.toList.foldK.orNotFound

  val serverResource = 
    BlazeServerBuilder[IO]
      .bindHttp(8080, "localhost")
      .withHttpApp(router)
      .resource
}

object HttpServerModule extends ModuleDef {
  make[HttpServer]
}

Now, let’s wire all the modules and create the server!

val finalModule = Seq(
    HomeRouteModule,
    BlogRouteModule,
    HttpServerModule,
  ).merge

val objects = Injector().produceUnsafe(finalModule, GCMode.NoGC)

val server = objects.get[HttpServer]

Let’s check if it works:

server.router.run(Request(uri = uri("/home")))
  .flatMap(_.as[String]).unsafeRunSync
// res10: String = "Home page!"
server.router.run(Request(uri = uri("/blog/1")))
  .flatMap(_.as[String]).unsafeRunSync
// res11: String = "Blog post ``1''!"

Fantastic!

See also, same concept in Guice.

Effect Bindings

Sometimes we need to effectfully create a component or fetch some data and inject it into the object graph during startup (e.g. read a configuration file), but the resulting component or data does not need to be closed. An example might be a global Semaphore that limits the parallelism of an entire application based on configuration value or a dummy/test double implementation of some external service made for testing using simple Refs.

In these cases we can use .fromEffect to simply bind a value created effectfully.

Example with ZIO Semaphore:

import distage._
import distage.config._
import zio._

case class Content(bytes: Array[Byte])

case class UploadConfig(maxParallelUploads: Long)

class UploaderModule extends ModuleDef {
  make[Semaphore].named("upload-limit").fromEffect {
    conf: UploadConfig @ConfPath("myapp.uploads") =>
      Semaphore.make(conf.maxParallelUploads)
  }
  
  make[Uploader]
}

class Uploader(limit: Semaphore @Id("upload-limit")) {
  def upload(content: Content): IO[Throwable, Unit] =
    limit.withPermit(upload(content))
}

Example with a Dummy KVStore:

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

object KVStore {
  def dummy: IO[Nothing, KVStore[IO]] = for {
    ref <- Ref.make(Map.empty[String, String])
    kvStore = new KVStore[IO] {
      def put(key: String, value: String): IO[Nothing, Unit] =
        ref.update(_ + (key -> value)).unit
      
      def get(key: String): IO[NoSuchElementException, String] = 
        for {
          map <- ref.get
          maybeValue = map.get(key)
          res <- maybeValue match {
            case None => 
              IO.fail(new NoSuchElementException(key))
            case Some(value) => 
              IO.succeed(value)
          }
        } yield res
    }
  } yield kvStore
}

val kvStoreModule = new ModuleDef {
  make[KVStore[IO]].fromEffect(KVStore.dummy)
}
// kvStoreModule: AnyRef with ModuleDef = Set(SingletonBinding({type.repl.Session::repl.Session.App12::repl.Session.App12.KVStore[=? %1:0,%1:1 ? ZIO[-Any,+1:0,+1:1]]},EffectImpl(repl.Session::repl.Session.App12::repl.Session.App12.KVStore[=? %1:0,%1:1 ? ZIO[-Any,+1:0,+1:1]],? %0 ? zio.ZIO[-Any,+Nothing,+0],InstanceImpl(zio.ZIO[-Any,+Nothing,+Session::App12::KVStore[=? %2:0,%2:1 ? ZIO[-Any,+2:0,+2:1]]],zio.ZIO$FlatMap@24edf7d)),Set(),(basics.md:438)))

new DefaultRuntime{}.unsafeRun {
  Injector().produceF[IO[Throwable, ?]](kvStoreModule, GCMode.NoGC)
    .use {
      objects =>
        val kv = objects.get[KVStore[IO]]
        
        for {
          _ <- kv.put("apple", "pie")
          res <- kv.get("apple")
        } yield res
    }
}
// res13: String = "pie"

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

Resource Bindings & Lifecycle

Lifecycle is supported via Resource bindings. You can inject any cats.effect.Resource into the object graph. You can also inject DIResource classes. Global resources will be deallocated when the app or the test ends.

Note that lifecycle control via DIResource is available in non-FP applications as well via inheritance from DIResource.Simple and DIResource.Mutable.

Example with cats Resource:

import distage._
import cats.effect._

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(
//   Map(
//     Delay(<function0>),
//     scala.Function1$$Lambda$19644/0x0000000844c11840@5d78ae5c,
//     1
//   )
// )

val mqResource = Resource.make(
  acquire = IO { println("Connecting to Message Queue!"); new MessageQueueConnection }
)(release = _ => IO(println("Disconnecting Message Queue")))
// mqResource: Resource[IO, MessageQueueConnection] = Allocate(
//   Map(
//     Delay(<function0>),
//     scala.Function1$$Lambda$19644/0x0000000844c11840@3660f744,
//     1
//   )
// )

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

def module = new ModuleDef {
  make[DBConnection].fromResource(dbResource)
  make[MessageQueueConnection].fromResource(mqResource)
  addImplicit[Bracket[IO, Throwable]]
  make[MyApp]
}

Will produce the following output:

Injector().produceF[IO](module, GCMode.NoGC).use {
  objects =>
    objects.get[MyApp].run
}.unsafeRunSync()
// Connecting to DB!
// Connecting to Message Queue!
// Hello World!
// Disconnecting Message Queue
// Disconnecting DB

Example with DIResource.Simple:

import distage._

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 = Set(SingletonBinding({type.repl.Session::repl.Session.App16::repl.Session.App16.Init},ResourceImpl(repl.Session::repl.Session.App16::repl.Session.App16.Init,? %0 ? 0,TypeImpl(repl.Session::repl.Session.App16::repl.Session.App16.InitResource)),Set(),(basics.md:554)))
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$App16$Init@45a370da
println(closedInit.initialized)
// false

DIResource forms a monad and has the expected .map, .flatMap, .evalMap methods available. You can convert a DIResource into a cats.effect.Resource via .toCats method.

You need to use resource-aware Injector.produce/Injector.produceF methods to control lifecycle of the object graph.

Injecting Implicits

TODO

Sorry, this page is not ready yet

Relevant ticket: https://github.com/7mind/izumi/issues/230

Implicits are managed like any other class. To make them available for summoning, declare them in a module:

import cats.Monad
import distage._
import zio.IO
import zio.interop.catz._

object IOMonad extends ModuleDef {
  addImplicit[Monad[IO[Throwable, ?]]]
  // same as make[Monad[IO[Throwable, ?]]].from(implicitly[Monad[IO[Throwable, ?]]])
}

Implicits for managed classes are injected from the object graph, NOT from the surrounding lexical scope. If they were captured from lexical scope inside ModuleDef, then classes would effectively depend on specific implementations of implicits available in scope at ModuleDef definition point. Depending on implementations is unmodular! We want to late-bind implicit dependencies same as any other dependencies, therefore you must specify implementations for implicits in ModuleDef.

import cats._
import distage._

trait KVStore[F[_]] {
  def fetch(key: String): F[String]
}

final class KVStoreEitherImpl(implicit F: MonadError[Either[Error, ?], Error]) extends KVStore[Either[Error, ?]] {
  def fetch(key: String) = F.raiseError(new Error(s"no value for key $key!"))
}

val kvstoreModuleBad = new ModuleDef {
  // We DON'T want this import to be necessary here
  // import cats.instances.either._

  make[KVStore[Either[Error, ?]]].from[KVStoreEitherImpl]
}

// Instead, wire implicits explicitly
val kvstoreModuleGood = new ModuleDef {

  make[KVStore[Either[Error, ?]]].from[KVStoreEitherImpl]
  
  // Ok to import here
  import cats.instances.either._
  
  // add the implicit dependency into the object graph
  addImplicit[MonadError[Either[Error, ?], Error]]
  
}

Implicits obey the usual lexical scope in user code.

You can participate in this ticket at https://github.com/7mind/izumi/issues/230

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:

As an 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._
import cats.implicits._
import distage._

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 ()
}

class Program[F[_]: TagK: Monad] extends 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 types that aren’t yet known at definition. You’ll need to add a TagK context bound to create a module parameterized by an abstract F[_]. Use Tag to create modules parameterized by non-higher-kinded types.

Interpreters:

import scala.util.Try
import cats.instances.all._

def tryValidation = new Validation[Try] {
  def minSize(s: String, n: Int): Try[Boolean] = Try(s.size >= n)
  def hasNumber(s: String): Try[Boolean] = Try(s.exists(c => "0123456789".contains(c)))
}
  
def tryInteraction = new Interaction[Try] {
  def tell(s: String): Try[Unit] = Try(println(s))
  def ask(s: String): Try[String] = Try("This could have been user input 1")
}

object TryInterpreters extends ModuleDef {
  make[Validation[Try]].from(tryValidation)
  make[Interaction[Try]].from(tryInteraction)
}

// combine all modules
val TryProgram = new Program[Try] ++ TryInterpreters
// TryProgram: Module = Set(SingletonBinding({type.repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %1:0 ? Try[+1:0]]},ProviderImpl({java.lang.Object & repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %1:0 ? Try[+1:0]]},izumi.distage.model.providers.ProviderMagnet$$$Lambda$18254/0x00000008446a3840@6bd557c(): {java.lang.Object & repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %1:0 ? Try[+1:0]]}),Set(),(basics.md:670)), SingletonBinding({type.cats.Monad[=? %1:0 ? Try[+1:0]]},InstanceImpl(cats.Monad[=? %1:0 ? Try[+1:0]],cats.instances.TryInstances$$anon$1@5303e50f),Set(),(basics.md:638)), SingletonBinding({type.repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %1:0 ? Try[+1:0]]},ProviderImpl({java.lang.Object & repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %1:0 ? Try[+1:0]]},izumi.distage.model.providers.ProviderMagnet$$$Lambda$18254/0x00000008446a3840@2a03e51(): {java.lang.Object & repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %1:0 ? Try[+1:0]]}),Set(),(basics.md:671)), SingletonBinding({type.repl.Session::repl.Session.App18::repl.Session.App18.TaglessProgram[=? %1:0 ? Try[+1:0]]},ProviderImpl(repl.Session::repl.Session.App18::repl.Session.App18.TaglessProgram[=? %1:0 ? Try[+1:0]],<function1>(cats.Monad[=? %1:0 ? Try[+1:0]], repl.Session::repl.Session.App18::repl.Session.App18.Validation[=? %1:0 ? Try[+1:0]], repl.Session::repl.Session.App18::repl.Session.App18.Interaction[=? %1:0 ? Try[+1:0]]): repl.Session::repl.Session.App18::repl.Session.App18.TaglessProgram[=? %1:0 ? Try[+1:0]]),Set(),(basics.md:636)))

// create object graph
val objects = Injector().produceUnsafe(TryProgram, GCMode.NoGC)
// objects: Locator = izumi.distage.LocatorDefaultImpl@16c3b30e

// run
objects.get[TaglessProgram[Try]].program
// awesomesauce!
// res19: Try[Unit] = Success(())

The program module is polymorphic over its eventual monad, we can easily parameterize it with a different monad:

import cats.effect._

def SyncInterpreters[F[_]: TagK](implicit F: Sync[F]) = new ModuleDef {
  make[Validation[F]].from(new 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)))
  })
  make[Interaction[F]].from(new 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 IOProgram = new Program[IO] ++ SyncInterpreters[IO]

We can leave it completely polymorphic as well:

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

Or choose different interpreters at runtime:

def DifferentTryInterpreters = ???
def chooseInterpreters(default: Boolean) = {
  val interpreters = if (default) TryInterpreters else DifferentTryInterpreters
  new Program[Try] ++ interpreters
}

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.

Testkit

distage-testkit module provides integration with scalatest:

libraryDependencies += Izumi.R.distage_testkit

or

libraryDependencies += "io.7mind.izumi" %% "distage-plugins" % "0.9.8-SNAPSHOT"

If you’re not using sbt-izumi-deps plugin.

Example usage:

```scala

```