Scala 3宏:如何调用在带引号的代码块中作为"Symbol“获得的方法?

wnrlj8wa  于 2023-02-04  发布在  Scala
关注(0)|答案(2)|浏览(160)

在一个带有类型参数T的Scala 3宏中,可以使用TypeRepr.of[T]和新的Scala 3 reflection API来探索TcompanionClass,并找到该伴随类上任意方法的Symbol(例如companionClass.declarations.find(_.name == "list")查找list()方法)。
给定伴随对象方法的Symbol,您将如何quoted code block中调用该方法
我猜我需要将Symbol转换为Expr[T],但我不知道如何做到这一点!
在Scala 2宏中,在q"..." quasiquote中调用c.universe.Symbol类型的listMethod看起来非常简单--只需要输入$listMethod,然后就可以开始Map结果列表,例如:

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

尝试在Scala 3宏中执行类似的操作会得到如下错误:

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

在Scala 3中使用正确的代码是什么?
您可以在这里查看AvroSerialisableMacro类中的更多代码上下文(Scala 2编译,Scala 3目前还远未完成!):https://github.com/guardian/marley/pull/77/files

eivgtgni

eivgtgni1#

首先,让我们讨论一下如何使用符号名调用一个方法。
您可能需要Select。您可以通过几种不同的方式调用获得它,例如:

New(TypeTree.of[YourType]).select(primaryConstructor) // when you want to create something

expression.select(method) // when you want to call it on something

选择方法后,可以提供参数:

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)

还有其他方法,如appliedToTypeappliedToTypeTrees,但如果您有一个方法名为Symbol,并希望使用它来调用某些内容,这应该是一个很好的起点。
请记住,Quotes的源代码是您的朋友,因此即使IDE没有给予您任何建议,它也可以为您指出一些解决方案。
理论上,这些方法是在Term上定义的,而不是在Select<: Term)上定义的,但是你的用例很可能是选择一个表达式,然后用一些参数调用它的方法。

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]

显然,证明expression可以调用method,并确保argsTerm的类型与传递给方法的值的允许类型相匹配,这是你的责任。这比Scala 2中的麻烦一些,因为引号只允许你使用Type[T]Expr[T]。因此,任何不属于该类别的内容都必须使用宏/Tasty ADT来实现,直到可以在${}中返回Expr为止。
也就是说,您链接的示例显示这些调用是相当硬编码的,因此您不必查找Symbol并调用它们。您的代码很可能会消除:

// 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)
        ): _*)
   ...
}
6ojccjat

6ojccjat2#

Scala 2 quasiquotes和Scala 3 quotations的区别在于前者必须在主代码的编译时使用宏进行编译(即,在宏扩展、宏运行时期间)而后者必须更早地编译,所以Scala 3的引号'{...}/${...}更像Scala 2的引号reify{...}/.splice,而不是Scala 2的准引号q"..."/${...}
tq equivalent in Scala 3 macros
你必须重新创建AST,让我们看看AST应该是什么形状:

object B:
  def fff(): Unit = ()

import scala.quoted.*

inline def foo(): Unit = ${fooImpl}

def fooImpl(using Quotes): Expr[Unit] =
  import quotes.reflect.*

  println('{B.fff()}.asTerm.show(using Printer.TreeStructure))

  '{()}
foo() // ... Apply(Select(Ident("B"), "fff"), Nil)

因此,为了重新创建AST,请尝试使用Apply(...)Select.unique(..., "list")

import scala.quoted.*

inline def foo[T](): Unit = ${fooImpl[T]}

def fooImpl[T: Type](using Quotes): Expr[Unit] =
  import quotes.reflect.*

  val sym = TypeRepr.of[T].typeSymbol

  '{
    println("aaa")

    ${
      Apply(
        Select.unique(
          Ref(sym.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

使用方法符号而不是名称,使用方便的方法而不是直接使用AST节点,可以将fooImpl重写为

def fooImpl[T: Type](using Quotes): Expr[Unit] =
  import quotes.reflect.*

  val sym = TypeRepr.of[T].typeSymbol
  val listMethod = sym.companionClass.declarations.find(_.name == "list").get

  '{
    println("aaa")

    ${
      Ref(sym.companionModule)
        .select(listMethod)
        .appliedToArgs(Nil)
        .asExprOf[Unit]
    }
  }

这只是一个如何创建AST的例子,在.asExprOf[Unit]中应该使用实际的返回类型def list()而不是Unit
How to get the list of default fields values for typed case class?
scala 3 macro how to implement generic trait

相关问题