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

13

在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)

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

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

或者至少这样:

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

但不是这个:

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

毋庸置疑,我正在寻找一个适用于任意情况类(而不仅仅是 Abc)的解决方案。如果需要使用宏,那也没问题。但请务必使用Scala 3。


我不清楚为什么您不喜欢使用productIterator?最终,这段代码被编译后,通过产品迭代器按名称或索引访问项目是相同的。 - Gaël J
1
当我反编译一个 case class 时,productElement 使用 if-then-else 实现,而 productIterator 在每一步都调用 productElement 实现。我理解这是一个很小的开销。但它仍然是运行时开销 (abc.productElement(1) 没有得到优化)。 - Koosha
1个回答

3

我提供一种解决方案,利用qoutes.reflect在宏展开期间进行。

使用qoutes.reflect可以检查传递的表达式。 在我们的情况下,我们想要找到字段名以便访问它(关于AST表示的一些信息,您可以阅读文档这里)。

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

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

在实现时,我们需要:
  • 获取对象中的所有字段
  • 访问字段
  • 打印每个字段

要访问对象字段(仅适用于 case classes),可以使用对象 Symbol 然后是方法 case fields。它会给我们一个 List,其中包含每个case字段的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的评论后,我尝试通过字段类型扩展示例选择方法。同样使用了宏(抱歉 :( ),我不知道如何在不反射代码的情况下选择属性字段。如果有什么技巧欢迎分享 :)

因此,除了第一个示例之外,在该示例中,我使用明确的类型类召唤和来自字段的类型。

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

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],我可以选择使用哪个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

1
感谢@gianluca提供的解决方案。是的,它可以工作,但我认为它不是100%正确的。原因如下:假设我想调用pp而不是println。有3个pp方法。第一个是def pp(value:Any):Unit = println(value)。第二个和第三个相同,除了value:Intvalue:String。现在,如果我写pp(abc.name),将调用具有value:String的方法。但是,在您的解决方案中,将调用value:Any。有什么想法如何解决这个问题? - Koosha
为了更加具体,当我写pp(abc.name)pp(abc.age)时,我甚至不需要pp(value: Any): Unit = println(value)存在。但是在宏版本中,如果有这个重载,代码将无法编译。 - Koosha
谢谢你的反馈:)。我非常感激。是的,我明白了,很抱歉,但我认为你只需要println方法的宏。顺便说一句,我认为你的问题可以通过隐式召唤和消耗某些类型的类型类(例如Show)来解决。如果你愿意,我可以用这个见解扩展这个例子:)。 - gianluca aguzzi
无宏定义?当然可以 :) - Koosha
1
@Koosha 抱歉,但我无法仅使用派生(带镜像)和类型类来解决它,可能我缺少一些信息。我知道使用低级API并不总是一个好主意,但我希望能在某种程度上帮助你 :) - gianluca aguzzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接