Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ out
.vscode
metals.sbt
Workpad.scala
.cursor
.cursor
.scala-build
26 changes: 21 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ There's a couple of wiring variants that you can choose from:
* `wire` create an instance of the given type, using dependencies from the context, within which it is called.
Dependencies are looked up in the enclosing trait/class/object and parents (via inheritance).
* `wireRec` is a variant of `wire`, which creates missing dependencies using constructors.
* `wireSet` collect all instances of the given type from the context and return them as a `Set`.
* `wireList` collect all instances of the given type from the context and return them as a `List`, preserving order.

In other words, `autowire` is context-free, while the `wire` family of macros is context-dependent.

Expand Down Expand Up @@ -56,7 +58,7 @@ To use, add the following dependency:
- [Accessing wired instances dynamically](#accessing-wired-instances-dynamically)
- [Limitations](#limitations)
- [Akka integration](#akka-integration)
- [Multi Wiring (wireSet)](#multi-wiring-wireset)
- [Multi Wiring (wireSet and wireList)](#multi-wiring-wireset-and-wirelist)
- [Autowire for cats-effect](#autowire-for-cats-effect)
- [Interceptors](#interceptors)
- [Qualifiers](#qualifiers)
Expand Down Expand Up @@ -724,29 +726,43 @@ object UserFinderActor {
val theUserFinder = wireActorWith(UserFinderActor.get _)("userFinder")
```

## Multi Wiring (wireSet)
## Multi Wiring (wireSet and wireList)

Using `wireSet` you can obtain a set of multiple instances of the same type. This is done without constructing the set explicitly. All instances of the same type which are found by MacWire are used to construct the set.

Consider the below example. Let's suppose that you want to create a `RockBand(musicians: Set[Musician])` object. It's easy to do so using the `wireSet` functionality:
Using `wireList` you can obtain a list of multiple instances of the same type, preserving the order of definition. This works similarly to `wireSet`, but returns a `List` instead of a `Set`, maintaining the order in which the instances are discovered during macro expansion. This method is available only in Scala 3.

Consider the below example. Let's suppose that you want to create a `RockBand` object with musicians:

```scala
trait Musician
class RockBand(musicians: Set[Musician])
class Orchestra(musicians: List[Musician])

trait RockBandModule {
lazy val singer = new Musician {}
lazy val guitarist = new Musician {}
lazy val drummer = new Musician {}
lazy val bassist = new Musician {}

lazy val musicians = wireSet[Musician] // all above musicians will be wired together
// musicians has type Set[Musician]
lazy val musiciansSet = wireSet[Musician] // all above musicians will be wired together
// musiciansSet has type Set[Musician] (unordered)

lazy val musiciansList = wireList[Musician] // all above musicians will be wired together
// musiciansList has type List[Musician] (preserves order)

lazy val rockBand = wire[RockBand]
lazy val orchestra = wire[Orchestra]
}
```

Both `wireSet` and `wireList` look for instances in the same places:
- enclosing members (lazy vals, vals, defs without parameters)
- enclosing imports
- parent classes and traits

The key difference is that `wireSet` returns an unordered `Set[T]` while `wireList` returns an ordered `List[T]` that preserves the order of definition discovery.

# Autowire for cats-effect

![Scala 2](https://img.shields.io/badge/Scala%202-8A2BE2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
addTo.find(_ == t).fold(t :: addTo)(_ => addTo)
}

trees.foldLeft(List.empty[Tree])(addIfUnique)
trees.foldLeft(List.empty[Tree])(addIfUnique).reverse // preserve order
}

def findInScope(tpe: TypeRepr, scope: Scope): Iterable[Tree] = {
Expand All @@ -116,7 +116,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
def findInAllScope(tpe: TypeRepr): Iterable[Tree] = {
@tailrec
def accInScope(scope: Scope, acc: List[Tree]): List[Tree] = {
val newAcc = doFindInScope(tpe, scope) ++ acc
val newAcc = acc ++ doFindInScope(tpe, scope)
if (!scope.isMax) accInScope(scope.widen, newAcc) else newAcc
}
uniqueTrees(accInScope(Scope.init, Nil))
Expand Down Expand Up @@ -163,7 +163,6 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
private def buildEligibleValue(symbol: Symbol, scope: Scope): PartialFunction[Tree, EligibleValues] = {
case m: ValDef =>
merge(
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m),
EligibleValues(
Map(
scope -> List(
Expand All @@ -173,11 +172,11 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
)
)
)
)
),
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m)
)
case m: DefDef if m.termParamss.flatMap(_.params).isEmpty =>
merge(
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m),
EligibleValues(
Map(
scope -> List(
Expand All @@ -187,7 +186,8 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
)
)
)
)
),
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m)
)
}

Expand All @@ -196,7 +196,12 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
)

def merge(ev1: EligibleValues, ev2: EligibleValues): EligibleValues =
EligibleValues((ev1.values.toSeq ++ ev2.values.toSeq).groupBy(_._1).view.mapValues(_.flatMap(_._2).toList).toMap)
EligibleValues(
(
for key <- ev1.values.keySet ++ ev2.values.keySet
yield key -> (ev1.values.getOrElse(key, Nil) ++ ev2.values.getOrElse(key, Nil))
).toMap
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,31 @@ object MacwireMacros {
code
}

def wireSet_impl[T: Type](using q: Quotes): Expr[Set[T]] = {
private def wireCollInstances[T: Type](using q: Quotes) = {
import q.reflect.*

val tpe = TypeRepr.of[T]
val dependencyResolver = DependencyResolver.throwErrorOnResolutionFailure[q.type, T](log)

val instances = dependencyResolver.resolveAll(tpe)
dependencyResolver.resolveAll(tpe)
}

def wireSet_impl[T: Type](using q: Quotes): Expr[Set[T]] = {
val instances = wireCollInstances[T]

val code = '{ ${ Expr.ofSeq(instances.toSeq.map(_.asExprOf[T])) }.toSet }

log(s"Generated code: ${code.show}")
code
}

def wireList_impl[T: Type](using q: Quotes): Expr[List[T]] = {
val instances = wireCollInstances[T]

val code = '{ ${ Expr.ofSeq(instances.toSeq.map(_.asExprOf[T])) }.toList }

log(s"Generated code: ${code.show}")
code
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ inline def wire[T]: T = ${ MacwireMacros.wireImpl[T] }
/** Collect all instances of the given type, available in the surrounding context (trait/class/object). */
inline def wireSet[T]: Set[T] = ${ MacwireMacros.wireSet_impl[T] }

/** Collect all instances of the given type, available in the surrounding context (trait/class/object), preserving the
* order of definition.
*/
inline def wireList[T]: List[T] = ${ MacwireMacros.wireList_impl[T] }

inline def wireWith[RES](inline factory: () => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
inline def wireWith[A, RES](inline factory: (A) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
inline def wireWith[A, B, RES](inline factory: (A, B) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
Expand Down
43 changes: 43 additions & 0 deletions tests/src/test/resources/test-cases/wireList.scala3.success
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
trait A
class A1 extends A
class A2 extends A
class A3 extends A
class A4 extends A

case class Group(as: List[A])

class Plugin1 {
lazy val a1: A = wire[A1]
}

class Plugin2 {
lazy val a2: A = wire[A2]
}

trait Module3 {
lazy val a3: A = wire[A3]
}

class App(plugin1: Plugin1, plugin2: Plugin2) extends Module3 {
import plugin1._
import plugin2._

lazy val a4: A = wire[A4]

// wireList looks at the same places as wireSet:
// - enclosing members
// - enclosing imports
// - parents
// but preserves the order of definition
lazy val as: List[A] = wireList[A]

// inject that List of A into Group
lazy val group: Group = wire[Group]
}

val plugin1 = new Plugin1
val plugin2 = new Plugin2
val app = new App(plugin1, plugin2)

require(app.group.as == List(plugin1.a1, plugin2.a2, app.a3, app.a4))

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include commonSimpleClasses

@Module class ModuleA1 { lazy val a: A = wire[A] }
@Module class ModuleA2 { lazy val a: A = wire[A] }

class AProvider { lazy val a: A = wire[A] }

class App(ma1: ModuleA1,
ma2: ModuleA2) {

lazy val as: List[A] = wireList[A] // should look into modules and preserve order
}

class App2(ma1: ModuleA1, aProvider: AProvider) {

// local definitions or imports shouldn't shadow modules
import aProvider.{a => aProvided}
lazy val a: A = wire[A]

lazy val as: List[A] = wireList[A] // => List(a, ma1.a, aProvided) in some deterministic order
}

object Test {
val ma1 = wire[ModuleA1]
val ma2 = wire[ModuleA2]
val aProvider = wire[AProvider]
val app = wire[App]
val app2 = wire[App2]
}

// Test that wireList finds the expected instances (same as wireSet but as List)
require(Test.app.as == List(Test.ma1.a, Test.ma2.a))
require(Test.app2.as == List(Test.ma1.a, Test.app2.a, Test.aProvider.a))