scalametaprogrammingscala-3

How to create an Instance of a generic class in Scala 3 Metaprogramming


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?


Solution

  • 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:

    so, when you make a progress with this issue, I'd encourage you to ask another question if you get stuck on another blocker.