Library is available for Scala 2.11, 2.12, 2.13-M4 and Scala.js 0.6 (Scala.js without 2.13.0-M4 due to a compiler bug in former ).
Add it with (2.11, 2.12):
libraryDependencies += "io.scalaland" %% "pulp" % "0.0.9"
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
or if you cross-build with Scala.js (2.11, 2.12):
libraryDependencies += "io.scalaland" %%% "pulp" % "0.0.9"
addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full)
or with Scala 2.13:
libraryDependencies += "io.scalaland" %% "pulp" % "0.0.9"
scalacOptions += "-Ymacro-annotations"
Latest version can be checked on Maven and is displayed on the badge above.
Ammonite users can try it out with:
import $ivy.`io.scalaland:pulp_2.12:0.0.9`, io.scalaland.pulp._
interp.load.plugin.ivy("org.scalamacros" % "paradise_2.12.4" % "2.1.0")
With Ammonite 1.1.0 you can try out this showoff code!
Pulp uses implicits Provider
type-class for dependency injection.
trait Provider[A] {
def get: A
}
For basic cases like
class A(b: B, c: C) {
..
}
it provides macro annotations that would create implicit def inside companion object
object A {
implicit def provider(implicit b: Provider[B], c: Provider[C]):
Provider[A] = ...
}
In this example as long as Providers for B
and C
will be in scope,
then Provider[C]
will return generated Provider[C]
while
Provider.get[C]
will return C
value.
As we can see this mechanism relies on propagating implicit Providers down the dependency hierarchy.
There are 4 flavors of macro annotation generating different Providers:
@Wired
- a default one. It generates following implementation:
def provider: Provider[A] = new Provider[A] { lazy val: A = ... }
it reuses computed `A` value in scope where it was generated, but across different scopes it might be generate a different instances of a type-class,
@Cached
- similar to default but it caches globally first instance
obtained for a WeakTypeTag
:
def provider: Provider[A] = new Provider[A] { def: A = internals.Cache.query(...) }
As long as two usages creates the same WeakTypeTag
it will reuse
first instance,
@Factory
- for factories. It generates following implementation:
def provider: Provider[A] = new Provider[A] { def: A = ... }
it guarantees to return new instance of A
each time get
is called,
@Singleton
- It generates following implementation:
lazy val provider: Provider[A] = new Provider[A] { lazy val: A = ... }
it guarantees to return the same instance of A
each time get
is called.
Probably the best would be to default to @Wired
and change them to
@Cached
, @Factory
or @Singleton
only where needed:
@Wired class NormalClass
@Cached class Storage
@Singleton class Database
@Factory class AsyncQueryBuilder
Macro annotations support more cases than semiauto-generated Provider
s:
@Wired class MultipleParamLists(param: String)(param2: Int)
@Factory class WithImplicit(value: Double)(implicit ec: ExecutionContext)
@Singleton class TypeBounded[F: Monad](init: F[String])
In case we want to split interface and implementation we can always use
trait A
@Wired class AImpl extends A
implicit aProvider: Provider[A] = Provider.upcast[AImpl, A]
However, in case both are defined in the same scope one would prefer to use just annotation for this:
@ImplemetendAs[AImpl] class A
@Wired class AImpl extends A
In case the class is a case class or the class has all its attributes public:
case class B (a: A)
class C (val a: A)
we might use derivation to generate provider using those available in scope:
import io.scalaland.pulp.semiauto._
Provider.get[B]
Provider.get[C]
However, we need to remember, that current scope of semiauto is limited. It does not support:
class A (i: Int)(d: Double)
) and implicit (class B (implicit ec: ExecutionContext)
, class C[F: Functor]
) - you need annotate the type to generate the provider,Provider
yourself, e.g. with Provider.const
or Provider.factory
,Generic
representation derived by Shapeless.
Macro annotations support it out of the box:
@ImplementedAs[ParametricImpl[A]] trait Parametric[A]
@Wired class ParametricImpl[A] extends Parametric[A]
Exception is the @Singleton
, which currently requires a monomorphic implementation:
@Singleton class DoubleParametric extends Parametric[Double] // ok
// @Singleton class AnyParametric[A] // doesn't compile
...are being automatically lifted to Provider
:
implicit val ec: ExecutionContext = ...
Provider.get[ExecutionContext]
Macro-generated Provider
s can be previewed during compilation with -Dpulp.debug=debug
or -Dpulp.debug=trace
SBT JVM flags.
I wanted to avoid runtime reflection based dependency injection in my program while still avoiding the need to pass everything manually. Existing ways of doing DI in Scala that I knew of were:
All of above have some pros and cons thought it's mostly up to programmers' taste to decide which trade off they like better (though they would often defend their own choice as the only reasonable).
I wanted to go with implicits, but that generating a bit of a boilerplate:
class A
class B (implicit a: A)
class C (implicit b: B)
class D (implicit b: B, c: C)
implicit val a: A = new A
implicit val b: B = new B
implicit val c: C = new C
Additionally we pollute the scope with tons of manually written implicits, including these passed by constructor.
Instead we could move them to companion objects and wrap in a dedicated type to ensure they won't accidentally mix with other implicits:
trait Provider[T] { def get(): T }
object Provider { def get[T: Provider]: T = implicitly[Provider[T]].get() }
class A
object A { implicit def provide: Provider[A] = () => new A }
class B (a: A)
object B { implicit def provide(implicit a: Provider[A]): Provider[B] = () => new B(a.get()) }
class C (b: B)
object B { implicit def provide(implicit b: Provider[B]): Provider[C] = () => new C(b.get()) }
class D (b: B, c: C)
object D { implicit def provide(implicit b: Provider[B], c: Provider[C]): Provider[D] = () => new D(b.get(), c.get()) }
Provider.get[D]
However, as we can see it brings a lot of boilerplate to the table. But what if we generated all of that code? E.g. with macro annotations:
@Wired class A
@Wired class B (a: A)
@Wired class C (b: B)
@Wired class D (b: B, c: C)
Provider.get[D]
That's basically what Pulp does.
import io.scalaland.pulp.semiauto._
Pulp uses implicits for passing objects around. It means that
Provider[T]
must be in scope of initialization for each dependency
required by our class. We might pass it manually, write implicit by hand
or take from companion object - remember however that only classes
annotated with @Wired
will have implicit Provider
s generated.
Additionally whether something will have one or more instances is not
guaranteed for @Wired
- if one need to ensure that there will be only
one Provider or that each Provider of some type will always return new
instance one should use @Singleton
or @Factory
. If there might be
arguments available in first usage scopes, Provider needs arguments from
scope, but @Singleton
doesn't work you might use @Cached
.
Last but not least such implementation of Provider
s is invariant - if
we have trait A
and @Wired class AImpl extends A
it will not be
resolved for A
unless we explicitly provide
implicit val a = Provider.upcast[AImpl, A]
or (if implementation is accessible to interface's scope):
@ImplementedAs[AImpl] class A
@Wired class AImpl extends A