Release Notes Maven Central Apache License 2.0 Test Workflow

# Scala Guide

Bali DI for Scala is a pure def macro which transforms the abstract syntax tree to automate dependency injection. It currently supports Scala 2.12 and 2.13, with Scala 3 on the roadmap.

This project is a sibling of Bali DI for Java. As a sibling, it is based on the exact same concepts and aims for eventual feature parity.

Bali is also an island (opens new window) between Java and Lombok in Indonesia. For disambiguation, the name of this project is "Bali DI", not just "Bali", where DI is an acronym for dependency injection (opens new window). In code however, the term "DI" is dropped because there is no ambiguity in this context.

# Getting Started

Bali DI for Scala is implemented as a def macro for the Scala compiler. If you use SBT, you need to add the following dependency to your project:

libraryDependencies += "global.namespace.bali" %% "bali-scala" % "0.5.3" % Provided

Note that this is a compile-time-only dependency - there is no runtime dependency of your code on Bali DI for Scala!

# Sample Code

This project uses a modular build. The module bali-scala-sample (opens new window) provides lots of sample code showing how to use the annotations and generated source code. The module is not published on Maven Central however - it is only available as source code. You can browse the source code here (opens new window) or clone this project.

The module is split into different packages with each package representing an individual, self-contained showcase. For example, the package bali.scala.sample.greeting (opens new window) showcases a glorified way to produce a simple "Hello world!" message by using different components with dependency injection.

# Tutorial

# WeatherStation

The source code shown in this section is available here (opens new window).

Let's define our first component:

trait Clock {

  def now: java.util.Date
}

In a component type, every abstract member is a dependency, so the Clock trait has a single dependency named now with type Date. We want our clock to return a fresh Date on every call, so now is declared as a def. This component doesn't have any domain logic, so there are no other members.

Next, let's look at a more advanced component and its companion object:

import bali.Lookup

trait Temperature[U <: Temperature.Unit] {

  @Lookup("tempValue")
  val value: Float // (1)

  @Lookup("tempUnit")
  val unit: U // (2)

  override def toString: String = f"$value%.1f˚ $unit"
}

object Temperature {

  sealed trait Unit

  type Celsius = Celsius.type
  object Celsius extends Unit { override def toString: String = "C" }

  type Fahrenheit = Fahrenheit.type
  object Fahrenheit extends Unit { override def toString: String = "F" }
}

The component trait Temperature has two abstract members and thus, two dependencies:

  1. value, with type Float.
  2. unit, with type U <: Temperature.Unit.

We want our temperature instances to be immutable, so both members are declared as a val.

The @Lookup annotation on each dependency defines an alias name for it. This is used to bind each dependency to some element in some module context - more on that later.

Next, let's look at our last component:

trait WeatherStation[U <: Temperature.Unit] extends Clock {

  def temp: Temperature[U]

  override def toString: String = s"$now: $temp"
}

This component also has two dependencies:

  1. now, with type Date, inherited from trait Clock.
  2. temp, with type Temperature[U].

We want our weather station to return a fresh Temperature on each call, so temp is declared as a def again.

Note that the component trait WeatherStation reuses the component trait Clock by inheritance and the component trait Temperature by composition: There is no limitation on how you arrange component types into a larger dependency graph. Because dependencies are resolved just-in-time, your dependency graph may even be circular!

Eventually, all the abstract methods in the components you've seen so far get implemented in a very simple manner by forwarding the call as-is to some module context. So for example, the abstract method now in the component trait Clock gets implemented as follows:

final override def now: Date = context.now

Likewise, if a dependency is declared as a val, then it gets implemented as a lazy val. So for example, the abstract method value in the component trait Temperature gets implemented as follows:

final override lazy val value: Float = context.tempValue

Note that the implementation respects the alias defined by the @Lookup("tempValue") annotation before.

Also, note that a dependency may have type parameters and parameter lists: The parameter lists are also forwarded in this case - more on that later.

In Scala, a module context is either:

  1. Any place in your code where you call the def macro bali.scala.make, or
  2. any type annotated with @bali.Module.

The make macro implements a module in place by creating an instance of an anonymous inner class which extends it type parameter and then implementing all abstract methods as shown before.

Let's look at an example:

import bali.scala.make

object MyApp extends App { context =>

  def now = new Date

  val clock = make[Clock] // (1)
}

The line with the call to make[Clock] gets expanded as follows:

val clock = new Clock {
  
  final override def now: Date = context.now
}

Note that if the type to make isn't abstract, then no anonymous inner class is created, and the default constructor is called instead. So for example, a call to make[Date] simply gets expanded to new Date.

With this in mind, let's look at the module type for our weather station:

import bali.Module
import java.util.concurrent.ThreadLocalRandom

@Module
trait WeatherStationModule {

  val april: WeatherStation[Temperature.Celsius] // (1)

  protected def now: Date // (2)

  protected def temp: Temperature[Temperature.Celsius] // (3)

  protected def tempValue: Float = ThreadLocalRandom.current.nextDouble(5d, 25d).toFloat

  protected final val tempUnit = Temperature.Celsius
}

Like a component type, a module type can also have abstract members (numbered 1 to 3 in this case). The difference is that in a module type, its abstract methods get implemented as a recursive call to the make macro. So for example, the abstract method now (no. 2 in the previous example) gets implemented as follows:

final override protected def now: Date = make[Date] // (2)

The call to make[Date] is then recursively expanded to new Date.

Why is this a recursive call? Because you need to call make[WeatherStationModule] in order to let it implement the method now in the first place!

Finally, let's look at some test code for our module:

import bali.scala.make
import bali.scala.sample.weatherstation.Temperature.Celsius
import org.scalatest.matchers.should.Matchers._
import org.scalatest.wordspec.AnyWordSpec

import java.util.Date

class WeatherStationModuleSpec extends AnyWordSpec {

  "A WeatherStationModule" should {
    val module = make[WeatherStationModule] // (1)
    import module._

    "report typical April weather" in {
      april.now should not be theSameInstanceAs(april.now)
      new Date should be <= april.now
      val temp = april.temp
      temp should not be theSameInstanceAs(april.temp)
      temp.value shouldBe temp.value
      temp.value should be >= 5f
      temp.value should be < 25f
      temp.unit shouldBe temp.unit
      temp.unit shouldBe Celsius
    }
  }
}

Putting all the puzzle pieces together, we can conclude that the line with the call to make[WeatherStationModule] gets expanded by the macro as follows:

val module = new WeatherStationModule {

  final override lazy val april: WeatherStation[Celsius] = make[WeatherStation[Celsius]]

  final override protected def now: Date = make[Date]

  final override protected def temp: Temperature[Celsius] = make[Temperature[Celsius]]
}

Next, another round is started to recursively expand the generated make[...] calls and form the final result:

val module = new WeatherStationModule { context =>

  final override lazy val april: WeatherStation[Celsius] = new WeatherStation[Celsius] {

    final override def temp: Temperature[Celsius] = context.temp

    final override def now: Date = context.now
  }

  final override protected def now: Date = new Date

  final override protected def temp: Temperature[Celsius] = new Temperature[Celsius] {

    final override lazy val value: Float = context.tempValue

    final override lazy val unit: Celsius = context.tempUnit
  }
}

As you can see, Bali DI automates the generation of a lot of boilerplate code for you, but there's much more to it than just that. For a general discussion of its design concept, features and benefits please check the Overview page.