Scala 3 迁移指南

混合 Scala 2.13 和 Scala 3 宏

语言

本教程展示如何在单个工件中混合 Scala 2.13 和 Scala 3 宏。这意味着使用者可以使用使用您宏的 Scala 2.13 代码中的“-Ytasty-reader”。

这有以下两个主要好处

  1. 让新的或现有的 scala 3 宏库对 Scala 2.13 用户可用,而无需提供单独的 2.13 版本
  2. 允许您的宏在模块逐个升级的多项目构建中使用。

简介

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)
  }
}

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

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

最终概述

您的库现在由以下部分组成

  • 包含混合宏定义和 Scala 3 宏实现的主要 Scala 3 模块。
  • 包含 Scala 2.13 宏实现的 Scala 2.13 兼容模块。它仅在编译器的宏扩展阶段在 Scala 2.13 中使用。

Mixing-macros Architecture

您现在可以发布您的库。

它可以在 Scala 3 项目或具有以下设置的 Scala 2.13 项目中使用

scalaVersion := "2.13.12"
libraryDependencies += ("org" %% "example" % "x.y.z").cross(CrossVersion.for2_13Use3)
scalacOptions += "-Ytasty-reader"

此页面的贡献者