I want to have a simple Service Discovery with Scala 3.
I use metaprogramming for this.
trait MyTrait
case class ClassA() extends MyTrait
case class ClassB() extends MyTrait
So it should instantiate ClassA
and ClassB
.
inline def findImplementations[T]: List[T] = ${ findImplementationsImpl[T] }
private def findImplementationsImpl[T: Type](using q: Quotes): Expr[List[T]] =
import quotes.reflect.*
val targetType = TypeRepr.of[T]
val rootPackage = Symbol.requiredPackage("camundala.worker")
val implementations: List[Symbol] = rootPackage.declarations
.collect {
case cls: ClassDef
if cls.isClassDef &&
cls.typeRef.derivesFrom(targetType.typeSymbol) &&
!cls.flags.is(Flags.Trait) =>
cls
}
.distinct
println(implementations) // List(class ClassA, class ClassB) -> this works as expected
val instanceExprs: List[Expr[T]] = implementations.map { symbol =>
val constructor: Symbol = symbol.primaryConstructor
val params = constructor.paramSymss.flatten
if params.isEmpty then
Apply(Select(New(TypeTree.of[T]), constructor), Nil).asExprOf[T] // PROBLEM!!
else
report.errorAndAbort(s"no suitable constructor found for ${symbol.name}, it ...")
end if
}
Expr.ofList(instanceExprs)
end findImplementationsImpl
And I call it like:
val implementations = findImplementations[MyTrait]
The problem is this part, New(TypeTree.of[T])
, because it tries to create MyTrait
instances, instead of the concrete classes.
Is there a way to achieve this?
Assuming you are only handling subtypes with default public constructors, you are mostly there. You only need to replace:
New(TypeTree.of[T])
with
New(treeOfTheSubtype)
You also already have the symbols of your subtypes, so you just need to translate the type symbol into TypeRepr
. I am doing it more or less like this:
def subtypeTypeOf[A: Type](subtype: Symbol): TypeRepr =
subtype.primaryConstructor.paramSymss match {
// subtype takes type parameters
case typeParamSymbols :: _ if typeParamSymbols.exists(_.isType) =>
// we have to figure how subtypes type params map to parent type params
val appliedTypeByParam: Map[String, TypeRepr] =
subtype.typeRef
.baseType(TypeRepr.of[A].typeSymbol)
.typeArgs
.map(_.typeSymbol.name)
.zip(TypeRepr.of[A].typeArgs)
.toMap
// TODO: some better error message if child has an extra type param that doesn't come from the parent
val typeParamReprs: List[TypeRepr] = typeParamSymbols
.map(_.name)
.map(appliedTypeByParam)
subtype.typeRef.appliedTo(typeParamReprs)
// subtype is monomorphic
case _ =>
subtype.typeRef
}
In your case that would be:
New(subtypeTypeOf[T](symbol)) // "symbol" is a val with subtype's Symbol
If you know that you are only working with a type that has no type parameters - subtype.typeRef
is all you need.
If there are some type parameters, you'd need the whole code because they could be applied in your Type[A]
but .typeRef
would always return a type constructor where type params has yet to be applied. Then you'd have to see what is the type applied to each type parameter name, and how names of type parameters in the subtype map to names in type parameters in the parent type, to reapply them.
BTW:
IMHE, checking if something has a default constructor is slightly more complicated, since:
isPublic
methodFlags.Trait
does not cover abstract classesso, when you make a progress with this issue, I'd encourage you to ask another question if you get stuck on another blocker.