此文件頁面專屬於 Scala 3,可能會涵蓋 Scala 2 中沒有的新概念。除非另有說明,此頁面中的所有程式碼範例都假設您使用的是 Scala 3。
內聯方法 透過在編譯時執行一些操作,為我們提供元編程的優雅技術。然而,有時內聯還不夠,我們需要更強大的方式來分析和綜合編譯時的程式。巨集使我們能夠做到這一點:將程式視為資料並加以操作。
巨集將程式視為值
使用巨集,我們可以將程式視為值,這讓我們可以在編譯時分析和產生它們。
具有類型 T
的 Scala 表達式由類型 scala.quoted.Expr[T]
的實例表示。
我們將深入探討類型 Expr[T]
的詳細資訊,以及分析和建構實例的不同方式,在討論 引號程式碼 和 反射 時。目前來說,只要知道巨集是操作類型 Expr[T]
的表達式的元程式即可。
以下巨集實作會在編譯器程式的標準輸出中列印提供引數的表達式,並在編譯時執行
import scala.quoted.* // imports Quotes, Expr
def inspectCode(x: Expr[Any])(using Quotes): Expr[Any] =
println(x.show)
x
在列印引數表達式後,我們會將原始引數傳回,作為類型 Expr[Any]
的 Scala 表達式。
如同 內聯 部分所預示的,內聯方法提供巨集定義的進入點
inline def inspect(inline x: Any): Any = ${ inspectCode('x) }
所有巨集都使用 inline def
定義。這個進入點的實作總是具有相同的形式
- 它們只包含一個 拼接
${ ... }
- 拼接包含對實作巨集的方法的單一呼叫(例如
inspectCode
)。 - 對巨集實作的呼叫會收到引號參數(即
'x
而不是x
)和一個語境Quotes
。
我們將在本文和後續章節中深入探討這些概念。
呼叫我們的 inspect
巨集 inspect(sys error "abort")
會在編譯時列印引數表達式的字串表示。
scala.sys.error("abort")
巨集和類型參數
如果巨集有類型參數,實作也需要知道這些參數。就像 scala.quoted.Expr[T]
代表類型為 T
的 Scala 表達式,我們使用 scala.quoted.Type[T]
來代表 Scala 類型 T
。
inline def logged[T](inline x: T): T = ${ loggedCode('x) }
def loggedCode[T](x: Expr[T])(using Type[T], Quotes): Expr[T] = ...
對應的內聯方法 (也就是 logged
) 中的 splice 會自動提供 Type[T]
的實例和脈絡 Quotes
,而且巨集實作可以使用這些實例和脈絡。
定義和使用巨集
內聯和巨集之間的一個關鍵差異在於它們的評估方式。內聯透過改寫程式碼並根據編譯器已知的規則進行最佳化來運作。另一方面,巨集會執行使用者撰寫的程式碼,而該程式碼會產生巨集擴充的程式碼。
技術上來說,編譯內聯程式碼 ${ inspectCode('x) }
會在編譯時 (透過 Java 反射) 呼叫方法 inspectCode
,然後方法 inspectCode
會像一般程式碼一樣執行。
為了能夠執行 inspectCode
,我們必須先編譯其原始碼。由於技術上的後果,我們無法在 **同一個類別/檔案** 中定義和使用巨集。不過,只要巨集的實作可以先編譯,就可以在 **同一個專案** 中定義巨集及其呼叫。
暫停檔案
為了允許在同一個專案中定義和使用巨集,只有那些已經編譯過的巨集呼叫才會展開。對於所有其他(未知的)巨集呼叫,檔案編譯會暫停。暫停的檔案只會在所有非暫停檔案都成功編譯後才會編譯。在某些情況下,您會有循環依賴關係,這會阻擋編譯完成。若要取得更多關於哪些檔案被暫停的資訊,您可以使用
-Xprint-suspension
編譯器旗標。
範例:使用巨集靜態評估 power
讓我們回顧一下我們在 內嵌 部分中對 power
的定義,它針對 n
的靜態已知值特化了 xⁿ
的運算。
inline def power(x: Double, inline n: Int): Double =
inline if n == 0 then 1.0
else inline if n % 2 == 1 then x * power(x, n - 1)
else power(x * x, n / 2)
在本部分的其餘部分,我們將定義一個巨集,用於針對靜態已知值 x
和 n
計算 xⁿ
。雖然這也可以純粹使用 inline
來完成,但使用巨集來實作它將說明一些事情。
inline def power(inline x: Double, inline n: Int) =
${ powerCode('x, 'n) }
def powerCode(
x: Expr[Double],
n: Expr[Int]
)(using Quotes): Expr[Double] = ...
簡單的表達式
我們可以如下實作 powerCode
def pow(x: Double, n: Int): Double =
if n == 0 then 1 else x * pow(x, n - 1)
def powerCode(
x: Expr[Double],
n: Expr[Int]
)(using Quotes): Expr[Double] =
val value: Double = pow(x.valueOrAbort, n.valueOrAbort)
Expr(value)
在此,pow
作業是一個簡單的 Scala 函式,用於計算 xⁿ
的值。有趣的部分是如何建立和查看 Expr
。
從值建立表達式
讓我們先來看看 Expr.apply(value)
。給定一個類型為 T
的值,此呼叫將回傳一個包含表示給定值的程式碼的表達式(也就是類型為 Expr[T]
)。傳遞給 Expr
的引數值是在編譯時計算的,在執行階段我們只需要實例化這個值。
從值建立表達式適用於所有基本類型、任何數性的組、Class
、Array
、Seq
、Set
、List
、Map
、Option
、Either
、BigInt
、BigDecimal
、StringContext
。如果實作了 ToExpr
,其他類型也可以使用,我們將會在稍後看到。
從表達式萃取值
我們在 powerCode
實作中使用的第二個方法是 Expr[T].valueOrAbort
,它具有與 Expr.apply
相反的效果。它嘗試從類型為 Expr[T]
的表達式中萃取一個類型為 T
的值。這只有在表達式直接包含一個值的程式碼時才會成功,否則它將擲出一個例外,停止巨集擴充並報告表達式與值不符。
除了 valueOrAbort
,我們也可以使用 value
操作,它會傳回一個 Option
。這樣我們就可以使用自訂錯誤訊息來回報錯誤。
回報自訂錯誤訊息
情境 Quotes
參數提供了一個 report
物件,我們可以使用它來回報自訂錯誤訊息。在巨集實作方法中,你可以使用 quotes
方法(使用 import scala.quoted.*
匯入)來存取情境 Quotes
參數,然後使用 import quotes.reflect.report
匯入 report
物件。
提供自訂錯誤
我們將在 report
物件上呼叫 errorAndAbort
來提供自訂錯誤訊息,如下所示
def powerCode(
x: Expr[Double],
n: Expr[Int]
)(using Quotes): Expr[Double] =
import quotes.reflect.report
(x.value, n.value) match
case (Some(base), Some(exponent)) =>
val value: Double = pow(base, exponent)
Expr(value)
case (Some(_), _) =>
report.errorAndAbort("Expected a known value for the exponent, but was " + n.show, n)
case _ =>
report.errorAndAbort("Expected a known value for the base, but was " + x.show, x)
或者,我們也可以使用 Expr.unapply
抽取器
...
(x, n) match
case (Expr(base), Expr(exponent)) =>
val value: Double = pow(base, exponent)
Expr(value)
case (Expr(_), _) => ...
case _ => ...
操作 value
、valueOrAbort
和 Expr.unapply
會對所有基本型別、任何元組、Option
、Seq
、Set
、Map
、Either
和 StringContext
運作。如果為其他型別實作了 FromExpr
,也可以運作,我們將會在後續章節中看到。
顯示表達式
在 inspectCode
的實作中,我們已經看過如何使用 .show
方法將表達式轉換為其原始碼的字串表示法。這對於對巨集實作執行除錯很有用
def debugPowerCode(
x: Expr[Double],
n: Expr[Int]
)(using Quotes): Expr[Double] =
println(
s"powerCode \n" +
s" x := ${x.show}\n" +
s" n := ${n.show}")
val code = powerCode(x, n)
println(s" code := ${code.show}")
code
使用變數參數
Scala 中的變數參數使用 Seq
表示,因此當我們使用變數參數撰寫巨集時,它會傳遞為 Expr[Seq[T]]
。可以使用 scala.quoted.Varargs
抽取器來還原每個個別引數(類型為 Expr[T]
)。
import scala.quoted.* // imports `Varargs`, `Quotes`, etc.
inline def sumNow(inline nums: Int*): Int =
${ sumCode('nums) }
def sumCode(nums: Expr[Seq[Int]])(using Quotes): Expr[Int] =
import quotes.reflect.report
nums match
case Varargs(numberExprs) => // numberExprs: Seq[Expr[Int]]
val numbers: Seq[Int] = numberExprs.map(_.valueOrAbort)
Expr(numbers.sum)
case _ => report.errorAndAbort(
"Expected explicit varargs sequence. " +
"Notation `args*` is not supported.", nums)
抽取器會比對對 sumNow(1, 2, 3)
的呼叫,並抽取包含每個參數程式碼的 Seq[Expr[Int]]
。但是,如果我們嘗試比對呼叫 sumNow(nums*)
的引數,抽取器將無法比對。
Varargs
也可用作建構函式。 Varargs(Expr(1), Expr(2), Expr(3))
會傳回 Expr[Seq[Int]]
。我們稍後會看到這如何派上用場。
複雜表達式
到目前為止,我們只看過如何建構和解構對應於簡單值的表達式。為了處理更複雜的表達式,Scala 3 提供了不同的元程式設計工具,範圍從
每個工具的複雜度越來越高,且可能會失去安全性保證。通常建議優先使用簡單的 API,而非更進階的 API。在本節的其餘部分,我們將介紹一些其他額外的建構函式和解構函式,而後續章節將介紹更進階的 API。
集合
我們已經看過如何使用 Expr.apply
將 List[Int]
轉換成 Expr[List[Int]]
。那如何將 List[Expr[Int]]
轉換成 Expr[List[Int]]
呢?我們提到 Varargs.apply
可以為序列執行此操作;同樣地,對於其他集合類型,也有對應的方法可用
Expr.ofList
:將List[Expr[T]]
轉換成Expr[List[T]]
Expr.ofSeq
:將Seq[Expr[T]]
轉換成Expr[Seq[T]]
(就像Varargs
)Expr.ofTupleFromSeq
:將Seq[Expr[T]]
轉換成Expr[Tuple]
Expr.ofTuple
:將(Expr[T1], ..., Expr[Tn])
轉換成Expr[(T1, ..., Tn)]
簡單區塊
建構函式 Expr.block
提供一種簡單的方法來建立一個程式碼區塊 { stat1; ...; statn; expr }
。它的第一個參數是一個包含所有陳述式的清單,第二個參數是區塊結尾的表達式。
inline def test(inline ignore: Boolean, computation: => Unit): Boolean =
${ testCode('ignore, 'computation) }
def testCode(ignore: Expr[Boolean], computation: Expr[Unit])(using Quotes) =
if ignore.valueOrAbort then Expr(false)
else Expr.block(List(computation), Expr(true))
當我們想要產生包含多個副作用的程式碼時,Expr.block
建構函式很有用。巨集呼叫 test(false, EXPRESSION)
會產生 { EXPRESSION; true}
,而呼叫 test(true, EXPRESSION)
會產生 false
。
簡單比對
方法 Expr.matches
可用於檢查一個表達式是否等於另一個表達式。使用此方法,我們可以針對 Expr[Boolean]
實作一個 value
函式,如下所示。
def value(boolExpr: Expr[Boolean]): Option[Boolean] =
if boolExpr.matches(Expr(true)) then Some(true)
else if boolExpr.matches(Expr(false)) then Some(false)
else None
它也可以用於比較兩個使用者撰寫的表達式。請注意,matches
只會執行有限度的正規化,而例如 Scala 表達式 2
會比對表達式 { 2 }
,這不適用於表達式 { val x: Int = 2; x }
。
任意表達式
最後但並非最不重要的一點,可以透過將任意 Scala 程式碼封裝在 引號中,從任意 Scala 程式碼建立一個 Expr[T]
。例如,'{ ${expr}; true }
會產生一個 Expr[Int]
,相當於 Expr.block(List(expr), Expr(true))
。後續關於 引號程式碼 的區段會更詳細地介紹引號。