在 Scala 3 的编译时提取和访问字段

Extracting and accessing fields at compile time in Scala 3

在 Scala 3 中的编译时 class 提取案例元素的名称和类型 已在此博客中得到很好的解释:https://blog.philipp-martini.de/blog/magic-mirror-scala3/ 但是,同一个博客使用 productElement 来获取实例中存储的值。我的问题是如何直接访问它们?考虑以下代码:

case class Abc(name: String, age: Int)
inline def printElems[A](inline value: A)(using m: Mirror.Of[A]): Unit = ???
val abc = Abc("my-name", 99)
printElems(abc)

您如何(更新 printElems 的签名并)实现 printElems 以便 printElems(abc) 将扩展为如下内容:

println(abc.name)
println(abc.age)

或至少这样:

println(abc._1())
println(abc._2())

但是不是这个:

println(abc.productElement(0))
println(abc.productElement(1))

不用说,我正在寻找适用于任意情况 classes 而不仅仅是 Abc 的解决方案。另外,如果必须使用宏,那也没关系。但请只使用 Scala 3。

我给你一个在宏扩展期间利用 qoutes.reflect 的解决方案。

使用 qoutes.reflect 可以检查传递的表达式。在我们的例子中,我们想要找到字段名称以便访问它(有关 AST 表示的一些信息,您可以阅读文档 here)。

所以,首先,我们需要构建一个内联定义,以便用宏扩展表达式:

inline def printFields[A](elem : A): Unit = ${printFieldsImpl[A]('elem)}

在实现中,我们需要:

  • 获取对象中的所有字段
  • 访问字段
  • 打印每个字段

要访问对象字段(仅适用于案例 classes),我们可以使用对象 Symbol,然后使用方法 case fields。它为我们提供了一个 List 填充每个案例字段的 Symbol 名称。

然后,要访问字段,我们需要使用Select(由反射模块给出)。它接受一个术语和存取符号。所以,例如,当我们写这样的东西时:

Select(term, field)

就像写代码一样:

term.field

最后,要打印每个字段,我们只能利用拼接。 总结一下,生成您需要的代码可能是:

import scala.quoted.*
def getPrintFields[T: Type](expr : Expr[T])(using Quotes): Expr[Any] = {
  import quotes.reflect._
  val fields = TypeTree.of[T].symbol.caseFields
  val accessors = fields.map(Select(expr.asTerm, _).asExpr)
  printAllElements(accessors)
}

def printAllElements(list : List[Expr[Any]])(using Quotes) : Expr[Unit] = list match {
  case head :: other => '{ println($head); ${ printAllElements(other)} }
  case _ => '{}
}

因此,如果您将其用作:

case class Dog(name : String, favoriteFood : String, age : Int)
Test.printFields(Dog("wof", "bone", 10))

控制台打印:

wof
bone
10

@koosha 的评论后,我尝试按字段类型扩展示例 selecting 方法。同样,我使用了宏(抱歉 :( ),我不知道如何在不反映代码的情况下 select 属性字段。如果有一些提示,欢迎 :)

所以,除了第一个示例,在这种情况下,我使用显式类型 classes 召唤并从字段中输入。

我创建了一个非常基本的类型 class:

trait Show[T] {
   def show(t : T) : Unit
}

以及一些实现:

implicit object StringShow extends Show[String] {
  inline def show(t : String) : Unit = println("String " + t)
}

implicit object AnyShow extends Show[Any] {
  inline def show(t : Any) : Unit = println("Any " + t)
}

AnyShow 被认为是故障安全默认值,如果在隐式解析期间没有发现其他隐式,我将使用它来打印元素。

可以使用 TypeRepTypeIdent

获取字段类型
val typeRep = TypeRepr.of[T]
val fields = TypeTree.of[T].symbol.caseFields
val fieldsType = fields.map(typeRep.memberType)
  .map(_.typeSymbol)
  .map(symbol => TypeIdent(symbol))
  .map(_.tpe)
  .map(_.asType)

现在,提供字段并利用 Expr.summon[T],我可以 select 使用 Show 的哪个实例:

val typeMirror = TypeTree.of[T]
val typeRep = TypeRepr.of[T]
val fields = TypeTree.of[T].symbol.caseFields
val fieldsType = fields.map(typeRep.memberType)
  .map(_.typeSymbol)
  .map(symbol => TypeIdent(symbol))
  .map(_.tpe)
  .map(_.asType)

fields.zip(fieldsType).map {
  case (field, '[t]) =>
  val result = Select(expr.asTerm, field).asExprOf[t]
    Expr.summon[Show[t]] match {
      case Some(show) =>
        '{$show.show($result)}
      case _ => '{ AnyShow.show($result) }
  }
}.fold('{})((acc, expr) => '{$acc; $expr}) // a easy way to combine expression

然后,您可以将其用作:

case class Dog(name : String, favoriteFood : String, age : Int)
printFields(Dog("wof", "bone", 10))

此代码打印:

String wof
String bone
Any 10