scalascala-macrosscala-3scala-reflectscala-quasiquotes

Scala 3 Macros: How to invoke a method obtained as a `Symbol` in a quoted code block?


In a Scala 3 macro that takes a type parameter T, you can use TypeRepr.of[T] and the new Scala 3 reflection API to explore the companionClass of T, and find the Symbol for an arbitrary method on that companion class (eg companionClass.declarations.find(_.name == "list") to find a list() method).

Given the Symbol for a companion object method, how would you then invoke that method within a quoted code block?

I'm guessing I would need to convert that Symbol to a Expr[T], but I don't know how to do that!

In a Scala 2 macro, the invocation of a listMethod of type c.universe.Symbol in a q"..." quasiquote seems pretty simple - just say $listMethod, and then you can start mapping on the resulting list, eg:

q"""
$listMethod.map(_.toString)
 """

Trying to do a similar thing in a Scala 3 macro gets an error like this:

[error] 27 |                ${listMethod}.map(_.toString)
[error]    |                  ^^^^^^^^^^
[error]    |                  Found:    (listMethod : x$1.reflect.Symbol)
[error]    |                  Required: quoted.Expr[Any]

What is the correct code to get this working in Scala 3?

You can see more code context in the AvroSerialisableMacro classes (Scala 2 compiles, Scala 3 currently nowhere near!) here: https://github.com/guardian/marley/pull/77/files


Solution

  • First, let's talk how to call a method using symbol name in general.

    You might need Select. You can call obtain it in a a few different ways, e.g.:

    New(TypeTree.of[YourType]).select(primaryConstructor) // when you want to create something
    
    expression.asTerm.select(method) // when you want to call it on something
    

    Once you selected method you can provide arguments:

    select.appliedToArgs(args)  // if there is only 1 argument list
    select.appliedToArgss(args) // if there is more than one argument list
                                // (type parameter list is listed in paramSymss
                                // but shouldn't be used here, so filter it out!)
    select.appliedToNone        // if this is a method like "def method(): T"
                                // (single, but empty, parameter list)
    select.appliedToArgss(Nil)  // is this is a method like "def method: T"
                                // (with not even empty parameter list)
    

    There are also other methods like appliedToType, appliedToTypeTrees, but if you have a method name as a Symbol and want to use it to call something this should be a good starting point.

    And remember that source code of Quotes is your friend, so even when your IDE doesn't give you any suggestions, it can point you towards some solution.

    In theory these methods are defined on Term rather than Select (<: Term) but your use case will be most likely picking an expression and calling a method on it with some parameters. So a full example could be e.g.

    val expression: Expr[Input]
    val method:     Symbol
    val args:       List[Term]
    
    // (input: Input).method(args) : Output
    expression             // Expr[Input]
      .asTerm              // Term
      .select(method)      // Select
      .appliedToArgs(args) // Term
      .asExpr              // Expr[?]
      .asExprOf[Output]    // Expr[Output]
    

    Obviously, proving that the expression can call method and making sure that types of Terms in args match allowed types of values that you pass to the method, is on you. It is a bit more hassle than it was in Scala 2 since quotes allow you to work with Type[T] and Expr[T] only, so anything that doesn't fall under that category has to be implemented with macros/Tasty ADT until you get to the point that you can return Expr inside ${}.

    That said, the example you linked shows that these calls are rather hardcoded, so you don't have to look up Symbols and call them. Your code will most likely do away with:

    // T <: ThriftEnum
    
    // Creating companion's Expr can be done with asExprOf called on
    // Ref from Dmytro Mitin's answer
    def findCompanionOfThisOrParent(): Expr[ThriftEnumObject[T]] = ...
    
    // _Expr_ with the List value you constructed instead of Symbol!
    val listOfValues: Expr[List[T]] = '{
      ${ findCompanionOfThisOrParent() }.list
    }
    
    // once you have an Expr you don't have to do any magic
    // to call a method on it, Quotes works nice
    '{
       ...
       val valueMap = Map(${ listOfValues }.map(x => x ->
              org.apache.avro.generic.GenericData.get.createEnum(
                com.gu.marley.enumsymbols.SnakesOnACamel.toSnake(x.name), schemaInstance)
            ): _*)
       ...
    }