使用宏的条件编译适用于方法,但不适用于字段
Conditional compilation with macros work for methods, but not for fields
我正在处理一个库 X,它依赖于另一个库 Y。为了支持 Y 的多个版本,X 发布了多个名为 X_Y1.0
、X_Y1.1
等的工件。这是在 SBT 中使用多个子项目完成的,这些子项目具有特定于版本的源目录,例如 src/main/scala-Y1.0
和 src/main/scala-Y1.1
.
到目前为止,它运行良好。一个小问题是有时版本特定的源目录太多了。有时它们需要大量的代码重复,因为在语法上不可能将微小的差异提取到单独的文件中。有时这样做会引入性能开销或使代码不可读。
为了解决这个问题,我添加了宏注释以选择性地删除部分代码。它是这样工作的:
class MyClass {
@UntilB1_0
def f: Int = 1
@SinceB1_1
def f: Int = 2
}
但是,它似乎只适用于方法。当我尝试在字段上使用宏时,编译失败并显示错误“f 已定义为值 f”。此外,它不适用于 类 和对象。
我怀疑宏是在解析方法重载之前的编译期间应用的,但在检查重名等基本检查之后。
有没有办法让宏对字段、类 和对象也起作用?
这是一个演示该问题的示例宏。
import scala.annotation.{compileTimeOnly, StaticAnnotation}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro paradise to expand macro annotations")
class Delete extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro DeleteMacro.impl
}
object DeleteMacro {
def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
c.Expr[Nothing](EmptyTree)
}
}
在方法上使用注释 @Delete
时,它会起作用。
class MyClass {
@Delete
def f: Int = 1
def f: Int = 2
}
// new MyClass().f == 2
但是,它不适用于字段。
class MyClass {
@Delete
val f: Int = 1
val f: Int = 2
}
// error: f is already defined as value f
首先,好主意:)
这是一种奇怪的(而且非常无法控制的)行为,我认为你想做的事情很难用宏来执行。
为了理解为什么你的扩展不起作用,我试着打印所有的 scalac 阶段。
你的扩展有效,确实给出了这个代码:
class Foo {
@Delete
lazy val x : Int = 12
val x : Int = 10
@Delete
def a : Int = 10
def a : Int = 12
}
typer
后打印的代码是:
package it.unibo {
class Foo extends scala.AnyRef {
def <init>(): it.unibo.Foo = {
Foo.super.<init>();
()
};
<empty>; //val removed
private[this] val x: Int = 10;
<stable> <accessor> def x: Int = Foo.this.x;
<empty>; //def removed
def a: Int = 12
};
...
}
但是,不幸的是,无论如何都会抛出错误,我将解释为什么会发生这种情况。
在 scalac 中,宏被扩展——至少在 Scala 2.13 中——在 packageobjects 阶段(因此在解析器和命名器阶段之后)。
在这里,会发生不同的事情,例如(如前所述here):
- 推断类型,
- 检查类型是否匹配,
- 搜索隐式参数并将它们添加到树中,
- 进行隐式转换,
- 检查是否允许所有类型操作(例如类型不能是其自身的子类型),
- 解决重载,
- 类型检查父引用,
- 检查类型违规,
- 搜索隐式,
- 扩展宏,
- 并为案例 类 创建额外的方法(如应用或复制)。
这里的本质问题是我们不能改变顺序,所以会发生在方法重载之前检查无效的val引用,而宏扩展发生在方法重载检查之前。由于这个原因,@delete 适用于方法,但不适用于 vals。
为了解决你的问题,我认为有必要使用compiler plugin,这里你可以在namer之前加一个phase,这样就不会报错了。 Build compiler plugin 编写宏比较困难,但我认为这是适合你的情况的最佳选择。
我正在处理一个库 X,它依赖于另一个库 Y。为了支持 Y 的多个版本,X 发布了多个名为 X_Y1.0
、X_Y1.1
等的工件。这是在 SBT 中使用多个子项目完成的,这些子项目具有特定于版本的源目录,例如 src/main/scala-Y1.0
和 src/main/scala-Y1.1
.
到目前为止,它运行良好。一个小问题是有时版本特定的源目录太多了。有时它们需要大量的代码重复,因为在语法上不可能将微小的差异提取到单独的文件中。有时这样做会引入性能开销或使代码不可读。
为了解决这个问题,我添加了宏注释以选择性地删除部分代码。它是这样工作的:
class MyClass {
@UntilB1_0
def f: Int = 1
@SinceB1_1
def f: Int = 2
}
但是,它似乎只适用于方法。当我尝试在字段上使用宏时,编译失败并显示错误“f 已定义为值 f”。此外,它不适用于 类 和对象。
我怀疑宏是在解析方法重载之前的编译期间应用的,但在检查重名等基本检查之后。
有没有办法让宏对字段、类 和对象也起作用?
这是一个演示该问题的示例宏。
import scala.annotation.{compileTimeOnly, StaticAnnotation}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
@compileTimeOnly("enable macro paradise to expand macro annotations")
class Delete extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro DeleteMacro.impl
}
object DeleteMacro {
def impl(c: blackbox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
c.Expr[Nothing](EmptyTree)
}
}
在方法上使用注释 @Delete
时,它会起作用。
class MyClass {
@Delete
def f: Int = 1
def f: Int = 2
}
// new MyClass().f == 2
但是,它不适用于字段。
class MyClass {
@Delete
val f: Int = 1
val f: Int = 2
}
// error: f is already defined as value f
首先,好主意:)
这是一种奇怪的(而且非常无法控制的)行为,我认为你想做的事情很难用宏来执行。
为了理解为什么你的扩展不起作用,我试着打印所有的 scalac 阶段。
你的扩展有效,确实给出了这个代码:
class Foo {
@Delete
lazy val x : Int = 12
val x : Int = 10
@Delete
def a : Int = 10
def a : Int = 12
}
typer
后打印的代码是:
package it.unibo {
class Foo extends scala.AnyRef {
def <init>(): it.unibo.Foo = {
Foo.super.<init>();
()
};
<empty>; //val removed
private[this] val x: Int = 10;
<stable> <accessor> def x: Int = Foo.this.x;
<empty>; //def removed
def a: Int = 12
};
...
}
但是,不幸的是,无论如何都会抛出错误,我将解释为什么会发生这种情况。
在 scalac 中,宏被扩展——至少在 Scala 2.13 中——在 packageobjects 阶段(因此在解析器和命名器阶段之后)。
在这里,会发生不同的事情,例如(如前所述here):
- 推断类型,
- 检查类型是否匹配,
- 搜索隐式参数并将它们添加到树中,
- 进行隐式转换,
- 检查是否允许所有类型操作(例如类型不能是其自身的子类型),
- 解决重载,
- 类型检查父引用,
- 检查类型违规,
- 搜索隐式,
- 扩展宏,
- 并为案例 类 创建额外的方法(如应用或复制)。
这里的本质问题是我们不能改变顺序,所以会发生在方法重载之前检查无效的val引用,而宏扩展发生在方法重载检查之前。由于这个原因,@delete 适用于方法,但不适用于 vals。
为了解决你的问题,我认为有必要使用compiler plugin,这里你可以在namer之前加一个phase,这样就不会报错了。 Build compiler plugin 编写宏比较困难,但我认为这是适合你的情况的最佳选择。