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 編譯器不包含 quasiquote 或 reification,則它可以編譯 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 中並使用 quasiquotes。

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"

此頁面的貢獻者