【问题标题】:Is it possible to using macro to modify the generated code of structural-typing instance invocation?是否可以使用宏来修改生成的结构化类型实例调用代码?
【发布时间】:2020-09-26 22:30:00
【问题描述】:

例如如下代码:

object Test extends App
{
    trait Class
    {
        val f1: Int
    }

    val c = new Class {
        val f1: Int = 1
        val f2: String = "Class"
    }

    println(c.f1)
    println(c.f2)
}

我使用反编译器查看字节码,并注意到编译生成了一个 java 接口“Test.Class”作为伪代码:

trait Class
{
    val f1: Int
}

还有一个实现“Test.Class”的类“Test$$anon$1”,伪代码为:

class Test$$anon$1 extends Class
{
    val f1: Int = 1
    val f2: String = "Class"
}

然后编译器将变量'c'初始化为:

c = new Test$$anon$1()

然后像正常调用一样调用成员“f1”:

println(c.f1)

但它使用反射调用“f2”:

println(reflMethod(c, f2))

这里,由于匿名类'Test$$anon$1'的定义在同一范围内可见,是否可以使用宏更改生成的代码以调用'f2'作为普通字段避免反射?

我只想更改同一范围内的调用代码,不想跨范围更改反射代码,例如结构类型实例作为函数调用中的参数。所以我觉得理论上是可以的。但我不熟悉 scala 宏,建议和代码示例表示赞赏。谢谢!

【问题讨论】:

  • Scala 宏必须生成可以编译的有效 Scala 代码。如果不是必须使用反射的东西,那么这里有效的 Scala 代码是什么?
  • 尽管如此,我怀疑这是可能的,但这不是一个好主意并且过度设计。如果您不想要结构类型,请不要使用它(定义 f2,在 Class 或显式子类型中)

标签: scala scala-macros structural-typing scalameta semanticdb


【解决方案1】:

宏(更准确地说,macro annotations 因为def macros 与此任务无关)是不够的。您想要重写的不是类(特征,对象)或其参数或成员,而是局部表达式。您可以在编译时使用compiler plugin(请参阅also)或在编译时使用Scalameta 代码生成来执行此操作。

如果您选择 Scalameta,那么实际上您希望在语义上而不是在语法上重写您的表达式,因为您想从本地表达式 new Class... 转到定义 trait Class... 并检查那里是否有适当的成员。所以你需要 Scalameta + SemanticDB。更方便的是使用 Scalameta + SemanticDB 和 Scalafix(另见 for users 部分)。

您可以创建自己的重写规则。然后,您可以使用它来就地重写代码或生成代码(见下文)。

rules/src/main/scala/MyRule.scala

import scalafix.v1._
import scala.meta._

class MyRule extends SemanticRule("MyRule") {
  override def isRewrite: Boolean = true

  override def description: String = "My Rule"

  override def fix(implicit doc: SemanticDocument): Patch = {
    doc.tree.collect {
      case tree @ q"new { ..$stats } with ..$inits { $self => ..$stats1 }" =>
        val symbols = stats1.collect {
          case q"..$mods val ..${List(p"$name")}: $tpeopt = $expr" =>
            name.syntax
        }

        val symbols1 = inits.headOption.flatMap(_.symbol.info).flatMap(_.signature match {
          case ClassSignature(type_parameters, parents, self, declarations) =>
            Some(declarations.map(_.symbol.displayName))
          case _ => None
        })

        symbols1 match {
          case None => Patch.empty
          case Some(symbols1) if symbols.forall(symbols1.contains) => Patch.empty
          case _ =>
            val anon = Type.fresh("anon$meta$")
            val tree1 =
              q"""
                class $anon extends ${template"{ ..$stats } with ..$inits { $self => ..$stats1 }"}
                new ${init"$anon()"}
              """
            Patch.replaceTree(tree, tree1.syntax)
        }
    }.asPatch
  }
}

在/src/main/scala/Test.scala

object Test extends App
{
  trait Class
  {
    val f1: Int
  }

  val c = new Class {
    val f1: Int = 1
    val f2: String = "Class"
  }

  println(c.f1)
  println(c.f2)
}

out/target/scala-2.13/src_managed/main/scala/Test.scala(在sbt out/compile之后)

object Test extends App
{
  trait Class
  {
    val f1: Int
  }

  val c = {
  class anon$meta$2 extends Class {
    val f1: Int = 1
    val f2: String = "Class"
  }
  new anon$meta$2()
}

  println(c.f1)
  println(c.f2)
}

build.sbt

name := "scalafix-codegen-demo"

inThisBuild(
  List(
    scalaVersion := "2.13.2",
    addCompilerPlugin(scalafixSemanticdb),
    scalacOptions ++= List(
      "-Yrangepos"
    )
  )
)

lazy val rules = project
  .settings(
    libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % "0.9.16"
  )

lazy val in = project

lazy val out = project
  .settings(
    sourceGenerators.in(Compile) += Def.taskDyn {
      val root = baseDirectory.in(ThisBuild).value.toURI.toString
      val from = sourceDirectory.in(in, Compile).value
      val to = sourceManaged.in(Compile).value
      val outFrom = from.toURI.toString.stripSuffix("/").stripPrefix(root)
      val outTo = to.toURI.toString.stripSuffix("/").stripPrefix(root)
      Def.task {
        scalafix
          .in(in, Compile)
//          .toTask(s" ProcedureSyntax --out-from=$outFrom --out-to=$outTo")
          .toTask(s" --rules=file:rules/src/main/scala/MyRule.scala --out-from=$outFrom --out-to=$outTo")
          .value
        (to ** "*.scala").get
      }
    }.taskValue
  )

project/plugins.sbt

addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.16")

其他例子:

https://github.com/olafurpg/scalafix-codegen

https://github.com/DmytroMitin/scalafix-codegen

https://github.com/DmytroMitin/scalameta-demo

Scala conditional compilation

Macro annotation to override toString of Scala function

How to merge multiple imports in scala?

【讨论】:

  • 非常感谢您提供全面的代码和参考资料!我会仔细研究这些。希望我能学到更多的scala技能并解决它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-25
  • 1970-01-01
  • 2020-04-25
  • 2018-10-18
  • 2011-08-04
  • 1970-01-01
  • 2022-08-10
相关资源
最近更新 更多