Jellyfish is a Scala library for dependency injection via delimited continuations.
To use Jellyfish, add the following to build.sbt:
libraryDependencies += "com.versal" %% "jellyfish" % "0.1.0"
First, write a program which retrieves dependencies via the read
function:
case class Foo(x: Int)
case class Bar(x: String)
object SimpleProgram {
import com.versal.jellyfish.{program, read}
// create a program with some dependencies
val simpleProgram =
program {
val bar: Bar = read[Bar] // retrieve the `Bar` dependency
val foo: Foo = read[Foo] // retrieve the `Foo` dependency
"foo is " + foo.x + ", bar is " + bar.x
}
}
Second, write an interpreter provides the dependencies to the program:
object SimpleInterpreter {
import com.versal.jellyfish.{classy, Program, Return, With}
val foo = Foo(42)
val bar = Bar("baz")
// run a program, injecting dependencies as needed
def run(p: Program): Any =
p match {
case With(c, f) if c.isA[Foo] => run(f(foo)) // inject the `Foo` dependency and continue
case With(c, f) if c.isA[Bar] => run(f(bar)) // inject the `Bar` dependency and continue
case Return(a) => a // all done - return the result
}
}
Third, run the interpreter:
val result = SimpleInterpreter.run(SimpleProgram.simpleProgram)
println(result) // prints "foo is 42, bar is baz"
A Jellyfish program is represented as an instance of the Program
trait, which has two implementations:
case class Return(a: Any) extends Program
case class With[A](c: Class[A], f: A => Program) extends Program
The read
function, which wraps Scala's shift
function, takes a generic function of type A => Program
and wraps it in a With
which tracks the type of A
. This can happen an arbitrary number of times, resulting in a data structure analogous to a curried function.
Ignoring some of the wrappers, this:
val bar: Bar = read[Bar] // retrieve the `Bar` dependency
val foo: Foo = read[Foo] // retrieve the `Foo` dependency
"foo is " + foo.x + ", bar is " + bar.x
becomes:
bar: Bar => {
val foo: Foo = read[Foo] // retrieve the `Foo` dependency
Return("foo is " + foo.x + ", bar is " + bar.x)
}
which becomes:
bar: Bar => {
foo: Foo => {
Return("foo is " + foo.x + ", bar is " + bar.x)
}
}
which is a curried function with two dependencies.
An interpreter is then built to unwrap each nested With
, extract the function of type A => Program
, provide the appropriate instance of A
, and continue until the program completes with a Return
.