必须从头开始重新实现宏库。
在开始之前,你应该熟悉 移植 sbt 项目 教程中所述的 Scala 3 迁移。本教程的目的是交叉构建现有的 Scala 2.13 宏库,以便在 Scala 3 和 Scala 2.13 中都可以使用它。
在 下一个教程 中解释了一种称为混合宏的替代解决方案。建议你阅读这两种解决方案,以选择最适合你需求的技术。
简介
为了举例说明本教程,我们将考虑下面定义的最小宏库。
// build.sbt
lazy val example = project
.in(file("example"))
.settings(
scalaVersion := "2.13.11",
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
)
// example/src/main/scala/location/Location.scala
package location
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
case class Location(path: String, line: Int)
object Macros {
def location: Location = macro locationImpl
private 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
方法通过使用宏 Context
并从该上下文中返回 Tree
来实现。
我们可以使用 sbt 提供的 交叉构建 技术,为 Scala 3 用户提供此库。
主要思想是构建工件两次,并发布两个版本
example_2.13
适用于 Scala 2.13 用户example_3
适用于 Scala 3 用户
1. 设置交叉构建
可以将 Scala 3 添加到项目的 crossScalaVersions
列表中
crossScalaVersions := Seq("2.13.11", "3.3.1")
scala-reflect
依赖项在 Scala 3 中不会有用。使用类似以下内容有条件地将其移除
// build.sbt
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, 13)) => Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
case _ => Seq.empty
}
}
重新加载 sbt 后,可以通过运行 ++3.3.1
切换到 Scala 3 上下文。在任何时候,都可以通过运行 ++2.13.11
返回到 Scala 2.13 上下文。
2. 在特定于版本的源目录中重新排列代码
如果尝试使用 Scala 3 编译,则应该看到一些与以下内容类似的错误
为了在保留 Scala 2 实现的同时提供 Scala 3 替代方案,我们将重新排列特定于版本的源目录中的代码。Scala 3 编译器无法编译的所有代码都转到 src/main/scala-2
文件夹。
特定于 Scala 版本的源目录是 sbt 的一项默认可用功能。在 sbt 文档 中了解有关它的更多信息。
在我们的示例中,Location
类保留在 src/main/scala
文件夹中,但 Macros
对象被移到 src/main/scala-2
文件夹
// example/src/main/scala-2/location/Macros.scala
package location
import scala.reflect.macros.blackbox.Context
import scala.language.experimental.macros
object Macros {
def location: Location = macro locationImpl
private 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)"
}
}
现在,我们可以在 src/main/scala-3
文件夹中初始化我们每个 Scala 3 宏定义。它们必须具有与 Scala 2.13 对应项完全相同的签名。
// example/src/main/scala-3/location/Macros.scala
package location
object Macros:
def location: Location = ???
3. 实现 Scala 3 宏
没有将 Scala 2 宏移植到 Scala 3 的神奇公式。需要了解新的 元编程 功能。
我们最终提出了此实现
// example/src/main/scala-3/location/Macros.scala
package location
import scala.quoted.{Quotes, Expr}
object Macros:
inline def location: Location = ${locationImpl}
private def locationImpl(using quotes: Quotes): Expr[Location] =
import quotes.reflect.Position
val pos = Position.ofMacroExpansion
val file = Expr(pos.sourceFile.jpath.toString)
val line = Expr(pos.startLine + 1)
'{new Location($file, $line)}
4. 交叉验证宏
添加一些测试非常重要,以检查宏方法在两个 Scala 版本中是否工作相同。
在我们的示例中,我们添加了一个测试。
现在你应该可以在两个版本中运行测试。
最终概述
宏项目现在应包含以下源文件
src/main/scala/*.scala
:交叉兼容类src/main/scala-2/*.scala
:宏方法的 Scala 2 实现src/main/scala-3/*.scala
:宏方法的 Scala 3 实现src/test/scala/*.scala
:通用测试
现在,您可以通过创建两个版本来发布库
example_2.13
适用于 Scala 2.13 用户example_3
适用于 Scala 3 用户