Dependency injection in Scala with Jellyfish

Scala Days 2013

James Earl Douglas

Alexy Khrabrov
@khrabrov

Versal
versal.com

Dependency lookup

val deps: Map[Class[_], Any] = Map(classOf[Repository] -> ... ,
                                   classOf[Logger] -> ... )
def getAllData: Data = {

  val log: Logger = deps(classOf[Logger])
  log.warn("this is going to take a while...")

  val repo: Repository = deps(classOf[Repository])
  repo.getData()

}

A good start, but where are the types?

Dependency lookup

Let's try embedding the dependencies in the function type

def getAllData: Logger => Repository => Data =
  { log =>

    log.warn("this is going to take a while...") ;

    { repo =>

      repo.getData()

    }
  }

Ok, but this is getting kind of callbacky.

Dependency lookup

Let's use a lookup, but keep the type information.

trait Program[+A]

def getAllData: Program[Data] = program {

  val log: Logger = read[Logger]
  log.warn("this is going to take a while...")

  val repo: Repository = read[Repository]
  repo.getData()

}

How do we run a Program, and where are the types?

Program interpreter

First, define what a Program can be.

case class With[A,B](c: Class[A], f: A => Program[B]) extends Program[B]
case class Return[A](a: A) extends Program[A]

Given a Program, inject any dependency needed to get a new Program. Repeat until done.

val deps: Map[Class[_], Any] = Map(classOf[Repository] -> ... ,
                                   classOf[Logger] -> ... )

def run[A](p: Program[A]): A = p match {
  case With(c, f) => run(f(deps(c)))
  case Return(a)  => a
}

How did we get a Program, anyway?

Delimited continuations

Using Scala's shift and reset functions.

def getAllData: Program[Data] = program {
  ...
}
def program[A](ctx: => Program[A] @program[Any]): Program[A] =
  reset(ctx).asInstanceOf[Program[A]]

def read[A](implicit mx: Manifest[A]) = shift { k: (A => Program[_]) =>
  With[A,Any](mx.erasure.asInstanceOf[Class[A]], k)
}

What does this buy us?

The good stuff

Source code

Jellyfish

Cannonball

An example Jellyfish-based J2EE Scala Web application

Cannonball

trait DbConnector { def connect[A](f: Connection => A): A }
trait IdGenerator { def generate: String }
def getEntries(offset: Int, limit: Int) = program {
  val dbConnector: DbConnector = read[DbConnector]
  val entries: Seq[Entry] = dbConnector.connect(Queries.getEntries(offset, limit))
  Return(entries)
}

def addEntry(content: String) = program {
  val dbConnector: DbConnector = read[DbConnector]
  val idGenerator: IdGenerator = read[IdGenerator]
  val id: String = idGenerator.generate
  val entry: Entry = dbConnector.connect(Commands.addEntry(id, content))
  Return(entry)
}