本教程展示如何在单个工件中混合 Scala 2.13 和 Scala 3 宏。这意味着使用者可以使用使用您宏的 Scala 2.13 代码中的“-Ytasty-reader”。
这有以下两个主要好处
- 让新的或现有的 scala 3 宏库对 Scala 2.13 用户可用,而无需提供单独的 2.13 版本
- 允许您的宏在模块逐个升级的多项目构建中使用。
简介
Scala 2.13 编译器只能扩展 Scala 2.13 宏,反之亦然,Scala 3 编译器只能扩展 Scala 3 宏。混合宏的想法是将两个宏打包到一个工件中,并在宏扩展阶段让编译器在两者之间进行选择。
这仅在 Scala 3 中才有可能,因为 Scala 3 编译器可以读取 Scala 3 和 Scala 2 定义。
让我们从考虑以下代码框架开始
// example/src/main/scala/location/Location.scala
package location
case class Location(path: String, line: Int)
object Macros:
def location: Location = macro ???
inline def location: Location = ${ ??? }
正如您所见,location
宏定义了两次
def location: Location = macro ???
是 Scala 2.13 宏定义inline def location: Location = ${ ??? }
是 Scala 3 宏定义
location
不是重载方法,因为这两个签名完全相同。这很令人惊讶!编译器如何接受具有相同名称和签名的两个方法?
解释是它识别出第一个定义仅适用于 Scala 2.13,第二个定义仅适用于 Scala 3。
1. 实现 Scala 3 宏
您可以将 Scala 3 宏实现与定义放在一起。
package location
import scala.quoted.{Quotes, Expr}
case class Location(path: String, line: Int)
object Macros:
def location: Location = macro ???
inline def location: Location = ${locationImpl}
private def locationImpl(using quotes: Quotes): Expr[Location] =
import quotes.reflect.Position
val file = Expr(Position.ofMacroExpansion.sourceFile.jpath.toString)
val line = Expr(Position.ofMacroExpansion.startLine + 1)
'{new Location($file, $line)}
2. 实现 Scala 2 宏
如果 Scala 3 编译器不包含任何准引号或重构,它可以编译 Scala 2 宏实现。
例如,这段代码可以用 Scala 3 编译,因此您可以将其与 Scala 3 实现放在一起。
import scala.reflect.macros.blackbox.Context
def locationImpl(c: Context): c.Tree = {
import c.universe._
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
New(c.mirror.staticClass(classOf[Location].getName()), path, line)
}
但是,在许多情况下,您必须将 Scala 2.13 宏实现移至 Scala 2.13 子模块中。
// build.sbt
lazy val example = project.in(file("example"))
.settings(
scalaVersion := "3.3.1"
)
.dependsOn(`example-compat`)
lazy val `example-compat` = project.in(file("example-compat"))
.settings(
scalaVersion := "2.13.12",
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
)
在此 example
中,我们的主库使用 Scala 3 编译,它依赖于使用 Scala 2.13 编译的 example-compat
。
在这种情况下,我们可以将 Scala 2 宏实现放入 example-compat
中并使用准引号。
package location
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
case class Location(path: String, line: Int)
object Scala2MacrosCompat {
private[location] def locationImpl(c: Context): c.Tree = {
import c.universe._
val location = typeOf[Location]
val line = Literal(Constant(c.enclosingPosition.line))
val path = Literal(Constant(c.enclosingPosition.source.path))
q"new $location($path, $line)"
}
}
请注意,我们必须将 Location
类向下移动。
3. 交叉验证宏
添加一些测试非常重要,以检查宏方法在两个 Scala 版本中是否工作相同。
由于我们希望在 Scala 2.13 和 Scala 3 中执行测试,因此我们在顶部创建了一个交叉构建的模块
// build.sbt
lazy val `example-test` = project.in(file("example-test"))
.settings(
scalaVersion := "3.3.1",
crossScalaVersions := Seq("3.3.1", "2.13.12"),
scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) => Seq("-Ytasty-reader")
case _ => Seq.empty
}
},
libraryDependencies += "org.scalameta" %% "munit" % "0.7.26" % Test
)
.dependsOn(example)
在 Scala 2.13 中需要
-Ytasty-reader
来使用 Scala 3 工件
例如,测试可以是
// example-test/src/test/scala/location/MacrosSpec.scala
package location
class MacrosSpec extends munit.FunSuite {
test("location") {
assertEquals(Macros.location.line, 5)
}
}
现在,您应该可以在两个版本中运行测试。
最终概述
您的库现在由以下部分组成
- 包含混合宏定义和 Scala 3 宏实现的主要 Scala 3 模块。
- 包含 Scala 2.13 宏实现的 Scala 2.13 兼容模块。它仅在编译器的宏扩展阶段在 Scala 2.13 中使用。
您现在可以发布您的库。
它可以在 Scala 3 项目或具有以下设置的 Scala 2.13 项目中使用
scalaVersion := "2.13.12"
libraryDependencies += ("org" %% "example" % "x.y.z").cross(CrossVersion.for2_13Use3)
scalacOptions += "-Ytasty-reader"