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
CodePudding user response:
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.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]
expression // Expr[Input]
.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 Term
s 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 Symbol
s 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)
): _*)
...
}
CodePudding user response:
You have to re-create AST. Try to use Select.unique(..., "list")
.
import scala.quoted.*
inline def foo[T](): Unit = ${fooImpl[T]}
def fooImpl[T: Type](using Quotes): Expr[Unit] =
import quotes.reflect.*
'{
println("aaa")
${
Apply(
Select.unique(
Ref(TypeRepr.of[T].typeSymbol.companionModule),
"list"
),
Nil
).asExprOf[Unit]
}
}
class A
object A {
def list(): Unit = println("list")
}
foo[A]()
//scalac: {
// scala.Predef.println("aaa")
// A.list()
//}
// prints at runtime:
// aaa
// list
How to get the list of default fields values for typed case class?