Skip to content

Commit 23404b6

Browse files
authored
Implement wireList (#413)
1 parent dac3744 commit 23404b6

File tree

7 files changed

+131
-15
lines changed

7 files changed

+131
-15
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ out
1010
.vscode
1111
metals.sbt
1212
Workpad.scala
13-
.cursor
13+
.cursor
14+
.scala-build

README.md

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ There's a couple of wiring variants that you can choose from:
1919
* `wire` create an instance of the given type, using dependencies from the context, within which it is called.
2020
Dependencies are looked up in the enclosing trait/class/object and parents (via inheritance).
2121
* `wireRec` is a variant of `wire`, which creates missing dependencies using constructors.
22+
* `wireSet` collect all instances of the given type from the context and return them as a `Set`.
23+
* `wireList` collect all instances of the given type from the context and return them as a `List`, preserving order.
2224

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

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

727-
## Multi Wiring (wireSet)
729+
## Multi Wiring (wireSet and wireList)
728730

729731
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.
730732

731-
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:
733+
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.
734+
735+
Consider the below example. Let's suppose that you want to create a `RockBand` object with musicians:
732736

733737
```scala
734738
trait Musician
735739
class RockBand(musicians: Set[Musician])
740+
class Orchestra(musicians: List[Musician])
736741

737742
trait RockBandModule {
738743
lazy val singer = new Musician {}
739744
lazy val guitarist = new Musician {}
740745
lazy val drummer = new Musician {}
741746
lazy val bassist = new Musician {}
742747

743-
lazy val musicians = wireSet[Musician] // all above musicians will be wired together
744-
// musicians has type Set[Musician]
748+
lazy val musiciansSet = wireSet[Musician] // all above musicians will be wired together
749+
// musiciansSet has type Set[Musician] (unordered)
750+
751+
lazy val musiciansList = wireList[Musician] // all above musicians will be wired together
752+
// musiciansList has type List[Musician] (preserves order)
745753

746754
lazy val rockBand = wire[RockBand]
755+
lazy val orchestra = wire[Orchestra]
747756
}
748757
```
749758

759+
Both `wireSet` and `wireList` look for instances in the same places:
760+
- enclosing members (lazy vals, vals, defs without parameters)
761+
- enclosing imports
762+
- parent classes and traits
763+
764+
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.
765+
750766
# Autowire for cats-effect
751767

752768
![Scala 2](https://img.shields.io/badge/Scala%202-8A2BE2)

macros/src/main/scala-3/com/softwaremill/macwire/internals/EligibleValuesFinder.scala

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
9292
addTo.find(_ == t).fold(t :: addTo)(_ => addTo)
9393
}
9494

95-
trees.foldLeft(List.empty[Tree])(addIfUnique)
95+
trees.foldLeft(List.empty[Tree])(addIfUnique).reverse // preserve order
9696
}
9797

9898
def findInScope(tpe: TypeRepr, scope: Scope): Iterable[Tree] = {
@@ -116,7 +116,7 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
116116
def findInAllScope(tpe: TypeRepr): Iterable[Tree] = {
117117
@tailrec
118118
def accInScope(scope: Scope, acc: List[Tree]): List[Tree] = {
119-
val newAcc = doFindInScope(tpe, scope) ++ acc
119+
val newAcc = acc ++ doFindInScope(tpe, scope)
120120
if (!scope.isMax) accInScope(scope.widen, newAcc) else newAcc
121121
}
122122
uniqueTrees(accInScope(Scope.init, Nil))
@@ -163,7 +163,6 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
163163
private def buildEligibleValue(symbol: Symbol, scope: Scope): PartialFunction[Tree, EligibleValues] = {
164164
case m: ValDef =>
165165
merge(
166-
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m),
167166
EligibleValues(
168167
Map(
169168
scope -> List(
@@ -173,11 +172,11 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
173172
)
174173
)
175174
)
176-
)
175+
),
176+
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.tpt.tpe), m)
177177
)
178178
case m: DefDef if m.termParamss.flatMap(_.params).isEmpty =>
179179
merge(
180-
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m),
181180
EligibleValues(
182181
Map(
183182
scope -> List(
@@ -187,7 +186,8 @@ private[macwire] class EligibleValuesFinder[Q <: Quotes](log: Logger)(using val
187186
)
188187
)
189188
)
190-
)
189+
),
190+
inspectModule(scope.widen, m.rhs.map(_.tpe).getOrElse(m.returnTpt.tpe), m)
191191
)
192192
}
193193

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

198198
def merge(ev1: EligibleValues, ev2: EligibleValues): EligibleValues =
199-
EligibleValues((ev1.values.toSeq ++ ev2.values.toSeq).groupBy(_._1).view.mapValues(_.flatMap(_._2).toList).toMap)
199+
EligibleValues(
200+
(
201+
for key <- ev1.values.keySet ++ ev2.values.keySet
202+
yield key -> (ev1.values.getOrElse(key, Nil) ++ ev2.values.getOrElse(key, Nil))
203+
).toMap
204+
)
200205
}
201206
}
202207

macros/src/main/scala-3/com/softwaremill/macwire/internals/MacwireMacros.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,31 @@ object MacwireMacros {
8787
code
8888
}
8989

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

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

96-
val instances = dependencyResolver.resolveAll(tpe)
96+
dependencyResolver.resolveAll(tpe)
97+
}
98+
99+
def wireSet_impl[T: Type](using q: Quotes): Expr[Set[T]] = {
100+
val instances = wireCollInstances[T]
97101

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

100104
log(s"Generated code: ${code.show}")
101105
code
102106
}
103107

108+
def wireList_impl[T: Type](using q: Quotes): Expr[List[T]] = {
109+
val instances = wireCollInstances[T]
110+
111+
val code = '{ ${ Expr.ofSeq(instances.toSeq.map(_.asExprOf[T])) }.toList }
112+
113+
log(s"Generated code: ${code.show}")
114+
code
115+
}
116+
104117
}

macros/src/main/scala-3/com/softwaremill/macwire/macwire.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ inline def wire[T]: T = ${ MacwireMacros.wireImpl[T] }
3131
/** Collect all instances of the given type, available in the surrounding context (trait/class/object). */
3232
inline def wireSet[T]: Set[T] = ${ MacwireMacros.wireSet_impl[T] }
3333

34+
/** Collect all instances of the given type, available in the surrounding context (trait/class/object), preserving the
35+
* order of definition.
36+
*/
37+
inline def wireList[T]: List[T] = ${ MacwireMacros.wireList_impl[T] }
38+
3439
inline def wireWith[RES](inline factory: () => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
3540
inline def wireWith[A, RES](inline factory: (A) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
3641
inline def wireWith[A, B, RES](inline factory: (A, B) => RES): RES = ${ MacwireMacros.wireWith_impl[RES]('factory) }
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
trait A
2+
class A1 extends A
3+
class A2 extends A
4+
class A3 extends A
5+
class A4 extends A
6+
7+
case class Group(as: List[A])
8+
9+
class Plugin1 {
10+
lazy val a1: A = wire[A1]
11+
}
12+
13+
class Plugin2 {
14+
lazy val a2: A = wire[A2]
15+
}
16+
17+
trait Module3 {
18+
lazy val a3: A = wire[A3]
19+
}
20+
21+
class App(plugin1: Plugin1, plugin2: Plugin2) extends Module3 {
22+
import plugin1._
23+
import plugin2._
24+
25+
lazy val a4: A = wire[A4]
26+
27+
// wireList looks at the same places as wireSet:
28+
// - enclosing members
29+
// - enclosing imports
30+
// - parents
31+
// but preserves the order of definition
32+
lazy val as: List[A] = wireList[A]
33+
34+
// inject that List of A into Group
35+
lazy val group: Group = wire[Group]
36+
}
37+
38+
val plugin1 = new Plugin1
39+
val plugin2 = new Plugin2
40+
val app = new App(plugin1, plugin2)
41+
42+
require(app.group.as == List(plugin1.a1, plugin2.a2, app.a3, app.a4))
43+
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include commonSimpleClasses
2+
3+
@Module class ModuleA1 { lazy val a: A = wire[A] }
4+
@Module class ModuleA2 { lazy val a: A = wire[A] }
5+
6+
class AProvider { lazy val a: A = wire[A] }
7+
8+
class App(ma1: ModuleA1,
9+
ma2: ModuleA2) {
10+
11+
lazy val as: List[A] = wireList[A] // should look into modules and preserve order
12+
}
13+
14+
class App2(ma1: ModuleA1, aProvider: AProvider) {
15+
16+
// local definitions or imports shouldn't shadow modules
17+
import aProvider.{a => aProvided}
18+
lazy val a: A = wire[A]
19+
20+
lazy val as: List[A] = wireList[A] // => List(a, ma1.a, aProvided) in some deterministic order
21+
}
22+
23+
object Test {
24+
val ma1 = wire[ModuleA1]
25+
val ma2 = wire[ModuleA2]
26+
val aProvider = wire[AProvider]
27+
val app = wire[App]
28+
val app2 = wire[App2]
29+
}
30+
31+
// Test that wireList finds the expected instances (same as wireSet but as List)
32+
require(Test.app.as == List(Test.ma1.a, Test.ma2.a))
33+
require(Test.app2.as == List(Test.ma1.a, Test.app2.a, Test.aProvider.a))

0 commit comments

Comments
 (0)