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 3 中,scala-reflect 相依性沒有用處。使用類似以下方式有條件地移除它

// 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 3 的替代方案,同時保留 Scala 2 的實作,我們將在特定版本的原始碼目錄中重新排列程式碼。所有無法由 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 使用者

此頁面的貢獻者