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

Koo*_*sha 9 scala scala-macros scala-3

在这个博客中已经很好地解释了在 Scala 3 编译时提取案例类的元素的名称和类型: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)
Run Code Online (Sandbox Code Playgroud)

如何(更新和的签名printElems)实现,printElems以便printElems(abc)将其扩展为如下所示:

println(abc.name)
println(abc.age)
Run Code Online (Sandbox Code Playgroud)

或者至少这个:

println(abc._1())
println(abc._2())
Run Code Online (Sandbox Code Playgroud)

不是这个:

println(abc.productElement(0))
println(abc.productElement(1))
Run Code Online (Sandbox Code Playgroud)

不用说,我正在寻找一种适用于任意 case 类的解决方案,而不仅仅是Abc. 此外,如果必须使用宏,那就没问题了。但请只使用 Scala 3。

gia*_*zzi 3

我为您提供了宏观扩张期间利用的解决方案qoutes.reflect

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

因此,首先,我们需要构建一个内联 def 以便用宏扩展表达式:

inline def printFields[A](elem : A): Unit = ${printFieldsImpl[A]('elem)}
Run Code Online (Sandbox Code Playgroud)

在实施过程中,我们需要:

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

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

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

Select(term, field)
Run Code Online (Sandbox Code Playgroud)

就像用代码编写类似的东西:

term.field
Run Code Online (Sandbox Code Playgroud)

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

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 _ => '{}
}
Run Code Online (Sandbox Code Playgroud)

因此,如果您将其用作:

case class Dog(name : String, favoriteFood : String, age : Int)
Test.printFields(Dog("wof", "bone", 10))
Run Code Online (Sandbox Code Playgroud)

控制台打印:

wof
bone
10
Run Code Online (Sandbox Code Playgroud)

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

因此,除了第一个示例之外,在本例中,我还使用显式类型类召唤并从字段中键入。

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

trait Show[T] {
   def show(t : T) : Unit
}
Run Code Online (Sandbox Code Playgroud)

以及一些实现:

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)
}
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

Show现在,给出该字段并利用 Expr.summon[T],我可以选择要使用的实例:

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
Run Code Online (Sandbox Code Playgroud)

然后,您可以将其用作:

case class Dog(name : String, favoriteFood : String, age : Int)
printFields(Dog("wof", "bone", 10))
Run Code Online (Sandbox Code Playgroud)

此代码打印:

String wof
String bone
Any 10
Run Code Online (Sandbox Code Playgroud)