本教學說明如何在單一成品中混合 Scala 2.13 和 Scala 3 巨集。這表示消費者可以使用您的巨集,從使用 Scala 2.13 程式碼的「-Ytasty-reader」。
這有兩個主要好處
- 讓新的或現有的 scala 3 巨集函式庫對 Scala 2.13 使用者可用,而無需提供單獨的 2.13 版本
- 允許您的巨集在正在逐模組升級的多專案建置中使用。
簡介
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 中使用。
您現在已準備好發佈您的函式庫。
它可以用在 Scala 3 專案,或在具有這些設定的 Scala 2.13 專案
scalaVersion := "2.13.12"
libraryDependencies += ("org" %% "example" % "x.y.z").cross(CrossVersion.for2_13Use3)
scalacOptions += "-Ytasty-reader"