此文件頁面特定於 Scala 2 中提供的功能,這些功能已在 Scala 3 中移除或由其他功能取代。除非另有說明,此頁面中的所有程式碼範例都假設您使用的是 Scala 2。
已過時
Eugene Burmako
在先前版本的 “巨集天堂” 中可用的類型巨集,在巨集天堂 2.0 中不再受支援。請參閱 天堂 2.0 公告,以取得說明和建議的移轉策略。
直覺
就像 def 巨集讓編譯器在看到某些方法的呼叫時執行自訂函數一樣,類型巨集讓人在使用某些類型時,可以掛接到編譯器。以下程式片段顯示 H2Db
巨集的定義和用法,它會產生代表資料庫中資料表的案例類別,以及簡單的 CRUD 功能。
type H2Db(url: String) = macro impl
object Db extends H2Db("coffees")
val brazilian = Db.Coffees.insert("Brazilian", 99, 0)
Db.Coffees.update(brazilian.copy(price = 10))
println(Db.Coffees.all)
完整的 H2Db
類型巨集原始碼已提供 在 GitHub,而本指南涵蓋其最重要的面向。首先,巨集會在編譯時連接到資料庫,以產生靜態類型資料庫包裝器(樹狀結構產生在 反射概觀 中說明)。然後它會使用 NEW c.introduceTopLevel
API,將產生的包裝器插入編譯器維護的頂層定義清單中。最後,巨集會傳回 Apply
節點,它代表對產生的類別的超級建構函數呼叫。注意,類型巨集應擴充為 c.Tree
,這與擴充為 c.Expr[T]
的 def 巨集不同。這是因為 Expr
代表術語,而類型巨集則擴充為類型。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val name = c.freshName(c.enclosingImpl.name).toTypeName
val clazz = ClassDef(..., Template(..., generateCode()))
c.introduceTopLevel(c.enclosingPackage.pid.toString, clazz)
val classRef = Select(c.enclosingPackage.pid, name)
Apply(classRef, List(Literal(Constant(c.eval(url)))))
}
object Db extends H2Db("coffees")
// equivalent to: object Db extends Db$1("coffees")
類型巨集可以透過傳回 Template
樹狀結構,來轉換其主機,而不是產生合成類別並擴充為對它的參考。在 scalac 內部,類別和物件定義都以薄封裝器表示為 Template
樹狀結構,因此透過擴充為範本,類型巨集有可能改寫受影響類別或物件的整個主體。您可以在 GitHub 上看到此技術的完整範例。
type H2Db(url: String) = macro impl
def impl(c: Context)(url: c.Expr[String]): c.Tree = {
val Template(_, _, existingCode) = c.enclosingTemplate
Template(..., existingCode ++ generateCode())
}
object Db extends H2Db("coffees")
// equivalent to: object Db {
// <existing code>
// <generated code>
// }
詳細資料
類型巨集代表 def 巨集和類型成員之間的混合體。一方面,它們的定義類似於方法(例如,它們可以有值引數、具有內容限制的類型參數等)。另一方面,它們屬於類型名稱空間,因此只能用於預期類型的地方(請參閱 GitHub 上的完整範例),它們只能覆寫類型或其他類型巨集等。
功能 | def 巨集 | 類型巨集 | 類型成員 |
---|---|---|---|
拆分為 def 和 impl | 是 | 是 | 否 |
可以有值參數 | 是 | 是 | 否 |
可以有類型參數 | 是 | 是 | 是 |
… 具有變異註解 | 否 | 否 | 是 |
… 具有內容限制 | 是 | 是 | 否 |
可以重載 | 是 | 是 | 否 |
可以繼承 | 是 | 是 | 是 |
可以覆寫和被覆寫 | 是 | 是 | 是 |
在 Scala 程式中,類型巨集可以出現在五種可能的職責中:類型職責、已套用類型職責、父類型職責、新職責和註解職責。巨集所使用的職責(可以使用 NEW c.macroRole
API 檢查)會影響其允許擴充清單的不同。
職責 | 範例 | 類別 | 非類別? | 套用? | 範本? |
---|---|---|---|---|---|
類型 | def x: TM(2)(3) = ??? |
是 | 是 | 否 | 否 |
套用類型 | class C[T: TM(2)(3)] |
是 | 是 | 否 | 否 |
父類型 | class C extends TM(2)(3) new TM(2)(3){} |
是 | 否 | 是 | 是 |
新建 | new TM(2)(3) |
是 | 否 | 是 | 否 |
註解 | @TM(2)(3) class C |
是 | 否 | 是 | 否 |
簡而言之,類型巨集的擴充套件會用它回傳的樹狀結構取代類型巨集的用法。若要找出擴充套件是否有意義,請在腦中將巨集的某個用法替換成其擴充套件,並檢查產生的程式是否正確。
例如,在 class C extends TM(2)(3)
中當 TM(2)(3)
用作類型巨集時,可以擴充套件為 Apply(Ident(TypeName("B")), List(Literal(Constant(2))))
,因為這會產生 class C extends B(2)
。不過,如果 TM(2)(3)
在 def x: TM(2)(3) = ???
中當作類型使用,相同的擴充套件就沒有意義,因為 def x: B(2) = ???
(假設 B
本身不是類型巨集;如果是,它會遞迴擴充套件,而擴充套件的結果會決定程式的有效性)。
提示和技巧
產生類別和物件
使用類型巨集時,你可能會越來越常發現自己處於 reify
不適用的區域,如 StackOverflow 中所說明。在這種情況下,請考慮使用 準引號,這是巨集天堂中的另一項實驗性功能,作為手動樹狀結構建構的替代方案。