【问题标题】:Getting a structural type with an anonymous class's methods from a macro从宏中获取具有匿名类方法的结构类型
【发布时间】:2026-02-10 03:55:01
【问题描述】:

假设我们要编写一个宏来定义一个具有一些类型成员或方法的匿名类,然后创建该类的一个实例,该类使用这些方法等静态类型化为结构类型。宏系统可以做到这一点在 2.10.0 中,类型成员部分非常简单:

object MacroExample extends ReflectionUtils {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  def foo(name: String): Any = macro foo_impl
  def foo_impl(c: Context)(name: c.Expr[String]) = {
    import c.universe._

    val Literal(Constant(lit: String)) = name.tree
    val anon = newTypeName(c.fresh)

    c.Expr(Block(
      ClassDef(
        Modifiers(Flag.FINAL), anon, Nil, Template(
          Nil, emptyValDef, List(
            constructor(c.universe),
            TypeDef(Modifiers(), newTypeName(lit), Nil, TypeTree(typeOf[Int]))
          )
        )
      ),
      Apply(Select(New(Ident(anon)), nme.CONSTRUCTOR), Nil)
    ))
  }
}

(其中ReflectionUtils 是一个convenience trait,它提供了我的constructor 方法。)

这个宏让我们可以将匿名类的类型成员的名称指定为字符串字面量:

scala> MacroExample.foo("T")
res0: AnyRef{type T = Int} = $1$$1@7da533f6

请注意,它的类型正确。我们可以确认一切正常:

scala> implicitly[res0.T =:= Int]
res1: =:=[res0.T,Int] = <function1>

现在假设我们尝试用一个方法做同样的事情:

def bar(name: String): Any = macro bar_impl
def bar_impl(c: Context)(name: c.Expr[String]) = {
  import c.universe._

  val Literal(Constant(lit: String)) = name.tree
  val anon = newTypeName(c.fresh)

  c.Expr(Block(
    ClassDef(
      Modifiers(Flag.FINAL), anon, Nil, Template(
        Nil, emptyValDef, List(
          constructor(c.universe),
          DefDef(
            Modifiers(), newTermName(lit), Nil, Nil, TypeTree(),
            c.literal(42).tree
          )
        )
      )
    ),
    Apply(Select(New(Ident(anon)), nme.CONSTRUCTOR), Nil)
  ))
}

但是当我们尝试它时,我们没有得到结构类型:

scala> MacroExample.bar("test")
res1: AnyRef = $1$$1@da12492

但如果我们在其中添加一个额外的匿名类:

def baz(name: String): Any = macro baz_impl
def baz_impl(c: Context)(name: c.Expr[String]) = {
  import c.universe._

  val Literal(Constant(lit: String)) = name.tree
  val anon = newTypeName(c.fresh)
  val wrapper = newTypeName(c.fresh)

  c.Expr(Block(
    ClassDef(
      Modifiers(), anon, Nil, Template(
        Nil, emptyValDef, List(
          constructor(c.universe),
          DefDef(
            Modifiers(), newTermName(lit), Nil, Nil, TypeTree(),
            c.literal(42).tree
          )
        )
      )
    ),
    ClassDef(
      Modifiers(Flag.FINAL), wrapper, Nil,
      Template(Ident(anon) :: Nil, emptyValDef, constructor(c.universe) :: Nil)
    ),
    Apply(Select(New(Ident(wrapper)), nme.CONSTRUCTOR), Nil)
  ))
}

有效:

scala> MacroExample.baz("test")
res0: AnyRef{def test: Int} = $2$$1@6663f834

scala> res0.test
res1: Int = 42

这非常方便——例如,它可以让你做this 之类的事情——但我不明白它为什么会起作用,并且类型成员版本可以工作,但不能bar。我知道这个may not be defined behavior,但这有什么意义吗?有没有更简洁的方法从宏中获取结构类型(及其上的方法)?

【问题讨论】:

  • 有趣的是,如果你在 REPL 中编写相同的代码而不是在宏中生成它,它可以工作: scala> { final class anon { def x = 2 };新匿名} res1: AnyRef{def x: Int} = anon$1@5295c398。感谢您的报告!我这周去看看。
  • 请注意,我已经提交了一个问题here
  • 不,不是阻止程序,谢谢——额外的匿名类技巧在我需要时对我有用。我刚刚注意到有人对这个问题投了赞成票,并对状态感到好奇。
  • 类型成员部分非常容易--> wTF?你真是太棒了!当然是好的方式:)
  • 这里有 153 个赞,issue on scala-lang.org 只有 1 个。更多的支持可能会更快地解决它?

标签: scala macros scala-2.10 structural-typing scala-macros


【解决方案1】:

Travis here 重复回答了这个问题。跟踪器中的问题和 Eugene 的讨论(在 cmets 和邮件列表中)都有链接。

在类型检查器著名的“Skylla and Charybdis”部分中,我们的英雄决定什么应该摆脱黑暗的匿名性,并将光明视为结构类型的一员。

有几种方法可以欺骗类型检查器(这不涉及 Odysseus 的抱羊策略)。最简单的方法是插入一个虚拟语句,使该块看起来不像是一个匿名类,然后是它的实例化。

如果打字员注意到你是一个没有被外界引用的公共术语,它会将你设为私有。

object Mac {
  import scala.language.experimental.macros
  import scala.reflect.macros.Context

  /* Make an instance of a structural type with the named member. */
  def bar(name: String): Any = macro bar_impl

  def bar_impl(c: Context)(name: c.Expr[String]) = {
    import c.universe._
    val anon = TypeName(c.freshName)
    // next week, val q"${s: String}" = name.tree
    val Literal(Constant(s: String)) = name.tree
    val A    = TermName(s)
    val dmmy = TermName(c.freshName)
    val tree = q"""
      class $anon {
        def $A(i: Int): Int = 2 * i
      }
      val $dmmy = 0
      new $anon
    """
      // other ploys
      //(new $anon).asInstanceOf[{ def $A(i: Int): Int }]
      // reference the member
      //val res = new $anon
      //val $dmmy = res.$A _
      //res
      // the canonical ploy
      //new $anon { }  // braces required
    c.Expr(tree)
  }
}

【讨论】:

  • 我要注意的是,我实际上在这个问题本身中提供了第一个解决方法(这里只是不准引用)。我很高兴得到这个答案来结束这个问题——我想我一直在模糊地等待这个错误得到修复。
  • @TravisBrown 我敢打赌你的蝙蝠腰带里还有其他工具。谢谢提醒:我认为您的 AST 是“旧的额外大括号技巧”,但我现在看到 ClassDef/Apply 没有像new $anon {} 那样包装在它们自己的块中。我的另一个收获是,将来我不会在带有准引号或类似特殊名称的宏中使用 anon
  • q"${s: String}" 语法有点延迟,尤其是在您使用天堂时。所以更像是下个月而不是下周。
  • @som-snytt @denys-shabalin,对于结构类型 a-la shapeless.Generic 是否有一种特殊的诡计?尽管我最好的意图是强制 Aux 模式返回类型,但编译器拒绝看穿结构类型。