Scala 3 迁移指南

交叉构建宏库

语言

必须从头开始重新实现宏库。

在开始之前,你应该熟悉 移植 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 用户

Cross-building Architecture

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 编译,则应该看到一些与以下内容类似的错误

sbt:example> ++3.3.1
sbt:example> example / compile
[error] -- Error: /example/src/main/scala/location/Location.scala:15:35 
[error] 15 |    val location = typeOf[Location]
[error]    |                                   ^
[error]    |                              No TypeTag available for location.Location
[error] -- Error: /example/src/main/scala/location/Location.scala:18:4 
[error] 18 |    q"new $location($path, $line)"
[error]    |    ^
[error]    |Scala 2 macro cannot be used in Dotty. See https://dotty.epfl.ch/docs/reference/dropped-features/macros.html
[error]    |To turn this error into a warning, pass -Xignore-scala2-macros to the compiler

为了在保留 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/location/Location.scala
package location

case class Location(path: String, line: Int)
// 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 版本中是否工作相同。

在我们的示例中,我们添加了一个测试。

// example/src/test/scala/location/MacrosSpec.scala
package location

class MacrosSpec extends munit.FunSuite {
  test("location") {
    assertEquals(Macros.location.line, 5)
  }
}

现在你应该可以在两个版本中运行测试。

sbt:example> ++2.13.11
sbt:example> example / test
location.MacrosSpec:
  + location
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success]
sbt:example> ++3.3.1
sbt:example> example / test
location.MacrosSpec:
  + location
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success]

最终概述

宏项目现在应包含以下源文件

  • src/main/scala/*.scala:交叉兼容类
  • src/main/scala-2/*.scala:宏方法的 Scala 2 实现
  • src/main/scala-3/*.scala:宏方法的 Scala 3 实现
  • src/test/scala/*.scala:通用测试

Cross-building Architecture

现在,您可以通过创建两个版本来发布库

  • example_2.13 适用于 Scala 2.13 用户
  • example_3 适用于 Scala 3 用户

此页面的贡献者