【问题标题】:Extracting and accessing fields at compile time in Scala 3在 Scala 3 中在编译时提取和访问字段
【发布时间】:2021-08-13 04:12:10
【问题描述】:

在 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 不适合您?最后编译这段代码,通过产品迭代器通过名称或索引访问项目是一样的。
  • 当我反编译一个case类时,productElement是使用if-then-else实现的,productIterator是通过每一步调用productElement实现的。我知道这是一个很小的开销。但这仍然是运行时开销(abc.productElement(1) 没有得到优化)。

标签: scala scala-macros scala-3


【解决方案1】:

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

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

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

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

在实现中,我们需要:

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

要访问对象字段(仅适用于案例类),我们可以使用对象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的评论之后,我尝试按字段类型扩展示例选择方法。再次,我使用了宏(对不起:(),我不知道如何选择属性字段而不反映代码。如果有一些提示,欢迎:)

所以,除了第一个例子,在这种情况下,我使用显式类型类从字段中调用和类型。

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

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

【讨论】:

  • 感谢@gianluca 的解决方案。是的,它有效,但我认为它不是 100% 正确的。原因如下:想象一下,我想打电话给pp,而不是println。有 3 个pp 方法。第一个是def pp(value: Any): Unit = println(value)。除了value: Intvalue: String 之外,第二和第三是相同的。现在,如果我写pp(abc.name),带有value: String 的那个会被调用。但在您的解决方案中,value: Any 将被调用。知道如何解决这个问题吗?
  • 更具体地说,当我写pp(abc.name)pp(abc.age) 时,我什至不需要pp(value: Any): Unit = println(value) 出现。但是在宏版本中,由于重载,代码将无法编译。
  • 感谢您的反馈 :)。我真的很感激。是的,我明白了,对不起,但我认为您只需要 println 方法的宏。顺便说一句,我认为您的问题可以使用隐式召唤和消耗某种类型的类型类(例如显示?)来解决。如果你愿意,我可以用这个见解扩展这个例子:)
  • 宏免费?是的,请:)
  • @Koosha 抱歉,但我无法仅使用派生(带镜像)和类型类来解决它,可能我缺少一些信息。我知道使用低级 API 并不总是一个好主意,但我希望以某种方式帮助你:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-06-17
  • 2021-07-31
  • 1970-01-01
相关资源
最近更新 更多