此文件頁面專門針對 Scala 2 中發布的功能,這些功能已在 Scala 3 中移除或由其他功能取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用的是 Scala 2。
巨集天堂
Eugene Burmako
巨集註解在 Scala 2.13 中可用,旗標為 -Ymacro-annotations
,且在 Scala 2.10.x 至 Scala 2.12.x 中可用,並搭配巨集天堂外掛程式。如果您使用的是較舊的 Scala 版本,請按照 “巨集天堂” 頁面的說明下載並使用我們的編譯器外掛程式。
請注意,需要同時編譯和擴充巨集註解時,需要巨集天堂外掛程式,這表示您的使用者也必須在他們的建置中加入巨集天堂,才能使用您的巨集註解。不過,在巨集註解擴充後,產生的程式碼將不再有任何巨集天堂的參考,且在編譯時間或執行時間都不需要它的存在。
逐步解說
巨集註解將文字抽象化帶到定義層級。使用 Scala 辨識為巨集的任何頂層或巢狀定義加上註解,將讓它擴充,可能擴充為多個成員。與巨集天堂的先前版本不同,2.0 中的巨集註解是正確執行的,因為它們:1) 不僅適用於類別和物件,也適用於任意定義,2) 允許類別擴充修改或甚至建立伴隨物件。這開啟了程式碼產生領域中許多新的可能性。
在此逐步解說中,我們將撰寫一個愚蠢但非常有用的巨集,除了記錄註解對象外,什麼都不做。作為第一步,我們定義一個繼承 StaticAnnotation
並定義 macroTransform
巨集的註解(macroTransform
名稱和 annottees: Any*
簽章對該巨集很重要,因為它們會告知巨集引擎,封閉註解是巨集註解)。
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
@compileTimeOnly("enable macro paradise to expand macro annotations")
class identity extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro ???
}
首先,請注意 @compileTimeOnly
註解。它不是強制性的,但建議使用以避免混淆。巨集註解看起來像香草 Scala 編譯器的正常註解,因此如果您忘記在建置中啟用巨集天堂外掛程式,您的註解將會靜默地無法擴充。@compileTimeOnly
註解可確保在 typer 之後,程式碼中不會出現對基礎定義的任何參考,因此它會防止上述情況發生。
現在,macroTransform
巨集應該取得未輸入類型註解的清單(在簽章中,其類型表示為 Any
,因為 Scala 中沒有更好的概念)並產生一個或多個結果(單一結果可以原樣傳回,多個結果必須包覆在 Block
中,因為反射 API 中沒有更好的概念)。
在這個時候,您可能會感到疑惑。單一註解和單一結果是可以理解的,但多對多的對應關係是什麼意思?有數個規則引導此程序
- 如果類別有註解,且它有伴隨物件,則兩者都會傳遞到巨集中。(但反之則不然 - 如果物件有註解,且它有伴隨類別,則只有物件本身會擴充)。
- 如果類別、方法或類型成員的參數有註解,則它會擴充其擁有者。首先是註解,然後是擁有者,再然後是伴隨物件,如前一規則所指定。
- 註解可以擴充到任何類型的樹狀結構,而編譯器會以透明的方式用巨集的輸出樹狀結構取代輸入樹狀結構。
- 如果類別擴充到同名的類別和模組,則它們會成為伴隨物件。這樣一來,即使沒有明確宣告伴隨物件,也可以為類別產生伴隨物件。
- 頂層擴充必須保留註解的數量、類型和名稱,唯一的例外是類別可能會擴充到同名的類別加上同名的模組,在這種情況下,它們會自動成為伴隨物件,如前一規則所述。
以下是 identity
標記巨集的可能實作。邏輯有點複雜,因為它需要考量 @identity
套用至值或類型參數的情況。請見諒我們採用低技術的解決方案,但我們並未將此樣板封裝在輔助程式中,因為編譯器外掛無法輕易變更標準函式庫。(順帶一提,此樣板可透過適當的標記巨集抽象化,我們很可能會在未來某個時間點提供此類巨集)。
import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.whitebox
@compileTimeOnly("enable macro paradise to expand macro annotations")
class identity extends StaticAnnotation {
def macroTransform(annottees: Any*): Any = macro identityMacro.impl
}
object identityMacro {
def impl(c: whitebox.Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
import c.universe._
val inputs = annottees.map(_.tree).toList
val (annottee, expandees) = inputs match {
case (param: ValDef) :: (rest @ (_ :: _)) => (param, rest)
case (param: TypeDef) :: (rest @ (_ :: _)) => (param, rest)
case _ => (EmptyTree, inputs)
}
println((annottee, expandees))
val outputs = expandees
c.Expr[Any](Block(outputs, Literal(Constant(()))))
}
}
範例程式碼 | 列印輸出 |
---|---|
@identity class C |
(<empty>, List(class C)) |
@identity class D; object D |
(<empty>, List(class D, object D)) |
class E; @identity object E |
(<empty>, List(object E)) |
def twice[@identity T] (@identity x: Int) = x * 2 |
(type T, List(def twice)) (val x: Int, List(def twice)) |
秉持 Scala 巨集的精神,巨集標記盡可能不具型別以保持彈性,並盡可能具型別以保持實用性。一方面,巨集標註對象不具型別,讓我們可以變更其簽章(例如類別成員清單)。但另一方面,所有類型的 Scala 巨集都與型別檢查器整合,而巨集標記也不例外。在擴充期間,我們可以取得所有可能的型別資訊(例如,我們可以針對周圍程式進行反映,或在封閉環境中執行型別檢查/隱式查詢)。