Dependency injection with Scala macros: auto-wiring

You can look at dependency injection as a fancy name for passing parameters to a function (or constructor arguments to a constructor). However usually, DI containers do much more than that. Among other things, one very nice feature is auto-wiring: instantiating the right objects with the right arguments. Most popular frameworks (Spring, Guice, CDI/Weld) accomplish this task at runtime using reflection.

[rant]
Doing the wiring at runtime with reflection has its downsides though. Firstly, there’s no compile-time checking that each dependency is satisfied. Secondly, we loose some of the flexibility we would have when doing things by hand, as we have to obey the rules by which the objects are created “automatically”. For example, if for some reason an object needs to be created manually, this requires a level of indirection (boilerplate), namely a factory. Finally, often the dependency injection is “global”, that is there is a single container with all the objects, it’s hard to create local/parametrized “universes” (Guice is an exception here). Finally-finally some frameworks do classpath scanning, which is slow, and sometimes can give unexpected results.
[/rant]

Way too magical for such a simple thing.

But isn’t what we really want just a way to have all the news with correct parameters generated for us? If you’re using Scala, and want code generation, the obvious answer are macros!

To finally show some code, given:

1
2
3
4
class A
class B
class C(a: A, b: B)
class D(b: B, c: C)

it would be nice to have:

1
2
3
4
val a    = wire[A]
val theB = wire[B] // "theB", not "b", just to show that we can use any name
val theC = wire[C]
val d    = wire[D]

transformed to:

1
2
3
4
val a    = new A()
val theB = new B()
val theC = new C(a, theB)
val d    = new D(theB, c)

Turns out it’s possible, and even not very complicated.

A proof-of-concept is available on GitHub. It’s very primitive and currently supports only one specific way of defining classes/wirings, but works :). If a dependency is missing, there’s a compile error. To check it out, simply clone the repo, run sbt and then invoke the task: run-main com.softwaremill.di.DiExampleRunner (implementation). During compilation, you should see some info messages regarding the generated code, e.g.:

1
2
3
[info] /Users/adamw/(...)/DiExample.scala:13: Generated code: new C(a, theB)
[info]   val c = wire[C]
[info]               ^

and then a proof that indeed the code was generated correctly: when the code is executed, the instances are printed to stdout so that you can see the arguments.

The macro here is of course the wire method (implementation). What it does is it first checks what are the parameters of the constructor of the class, and then for each parameter, tries to find a val defined in the enclosing class of the desired type (findWiredOfType method; see also this StackOverflow question why the search is limited to the enclosing class). Finally, it assembles a tree corresponding to invoking the constructor with the right arguments:

1
2
3
Apply(
   Select(New(Ident([class's type])), nme.CONSTRUCTOR), 
   List(Ident([arg1]), Ident([arg2]), ...))

This concept can be extended in many ways. Firstly, by adding support for sub-typing (now only exact type matches will work). Then, there’s the ability to define the wirings not only in a class, but also in methods; or extending the search to mixed-in traits, so that you could split the wire definitions among multiple traits (“modules”?). Notice that we could also have full flexibility in how we access the wired valued; it could be a val, lazy val or a def. There’s also support for scoping, factories, singletons, configurations values, …; for example:

(Dependency Injection of the future!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// "scopes"
val a = wire[X]
lazy val b = wire[Y]
def c = wire[Z] 
val d = provided(manuallyCreatedInstance)
 
// override a single dependency
val a = wire[X].with(anotherYInstance)
 
// factories: p1, p2, ... are used in the constructor where needed
def e(p1: T, p2: U, ...) = wire[X] 
 
// by-name binding for configuration parameters; whenever a class has a
// "maxConnections" constructor argument, this value is used.
val maxConnections = conf(10)

A recent project by Guice’s creator, Bob Lee, goes in the same direction. Dagger (mainly targeted at Android as far as I know) uses an annotation processor to generate the wiring code; at runtime, it’s just plain constructor invocations, no reflection. Similarly here, with the difference that we use Scala’s macros.

What do you think of such an approach to DI?

Adam

  • Brian Vidal

    Say I have a RealDAO and a MockDAO.
    I still don’t know how would a macro choose from them when I’m testing or running in production.

  • http://www.warski.org/ Adam Warski

    Quite simple:


    val userDao = if (production) wire[MongoUserDao] else wire[MockDao]

    or if the dao is created manually:


    val userDao = if (production) wire[MongoUserDao] else provided(mockDao)

    “production” is a boolean flag read e.g. from configuration.

  • http://www.warski.org/ Adam Warski

    Or better, if you want to separate production and test beans:


    trait ProductionBeans {
    val bean1 = wire[Bean1]
    val bean2 = wire[Bean2]
    val daoBean1 = wire[DaoBean1]
    val daoBean2 = wire[DaoBean2]
    }

    trait TestBeans extends ProductionBeans {
    override val daoBean1 = wire[MockDaoBean1]
    }

  • Pingback: MacWire 0.1: Framework-less Dependency Injection with Scala Macros | Blog of Adam Warski()

  • Juan Pablo

    I’ve got this code working


    package co.s4n.school

    class UsuarioDAO(){
    def getUseById(id:Int)="User from DB"
    }

    trait DaoModule {
    import com.softwaremill.macwire._
    lazy val dao = wire[UsuarioDAO]
    }

    with the following test

    package co.s4n.school

    class miTestDAO() extends UsuarioDAO {
    override def getUseById(id:Int) = "Mocked User :)"
    }

    trait testModule extends DaoModule{
    override lazy val dao = new miTestDAO()
    }

    object Test extends App{
    val cliente = new Cliente() with testModule
    val resultadoOpCompleja = cliente.operacionCompleja
    println(resultadoOpCompleja)
    }

    However it works the same when the

    lazy val dao = wire[UsuarioDAO]

    Is replaced with:


    lazy val dao = new UsuarioDAO

    What's the difference and why should I use the wire[UsuarioDAO] ?

  • Juan Pablo

    I’ve got this code working


    package co.s4n.school

    class UsuarioDAO(){
    def getUseById(id:Int)="User from DB"
    }

    trait DaoModule {
    import com.softwaremill.macwire._
    lazy val dao = wire[UsuarioDAO]
    }

    with the following test


    package co.s4n.school

    class miTestDAO() extends UsuarioDAO {
    override def getUseById(id:Int) = "Mocked User :)"
    }

    trait testModule extends DaoModule{
    override lazy val dao = new miTestDAO()
    }

    object Test extends App{
    val cliente = new Cliente() with testModule
    val resultadoOpCompleja = cliente.operacionCompleja
    println(resultadoOpCompleja)
    }

    However it works the same when the


    lazy val dao = wire[UsuarioDAO]

    Is replaced with:


    lazy val dao = new UsuarioDAO

    What’s the difference and why should I use the wire[UsuarioDAO]

  • http://www.warski.org/ Adam Warski

    `wire` is a macro and at compile-time, the `wire[UsuariaDAO]` will get replaced with exactly `new UsuarioDAO`! You don’t have any dependencies here, but `wire` is useful when there are dependencies, as they are automatically looked up and the right new invocation is created.

    However, it is perfectly fine to do manual DI as well! Please see http://di-in-scala.github.io

  • Juan Pablo

    Got it. I’ve added a dependency. I get how wire[] helps in not writing ‘new’ but I know I’m missing something because if I build the dependencies with ‘new’ and then I stack some additional traits with the mock implementations then the ‘injection’ is handled in a cake fashion.

    I know I’m missing something to understand the power of macwire :)

    package co.s4n.school.macwire

    class UsuarioDAO(){

    def getUseById(id:Int)="User from DB"

    }

    class UsuarioRepo(dao:UsuarioDAO){

    def getUserById(id:Int):String=dao.getUseById(id)

    }

    trait DaoModule {

    import com.softwaremill.macwire._

    //lazy val dao = wire[UsuarioDAO]

    //lazy val repo = wire[UsuarioRepo]

    lazy val dao = new UsuarioDAO()

    lazy val repo = new UsuarioRepo(dao)

    }

  • http://www.warski.org/ Adam Warski

    Again wrapping partial object graphs in traits is entirely optional and probably only makes sense for larger applications. And you are right, it’s exactly the same idea as in Cake (hence the name – Think Cake Pattern), however without the need to put the actual implementations inside the traits.

    Here the trait-modules only contain the wiring of a part of an object graph, plus possibly some module-external dependencies. You don’t need a trait-per-component, as in Cake, but you get the composition benefits that Cake gives you