巨集

巨集註解

語言
此文件頁面專門針對 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 中沒有更好的概念)。

在這個時候,您可能會感到疑惑。單一註解和單一結果是可以理解的,但多對多的對應關係是什麼意思?有數個規則引導此程序

  1. 如果類別有註解,且它有伴隨物件,則兩者都會傳遞到巨集中。(但反之則不然 - 如果物件有註解,且它有伴隨類別,則只有物件本身會擴充)。
  2. 如果類別、方法或類型成員的參數有註解,則它會擴充其擁有者。首先是註解,然後是擁有者,再然後是伴隨物件,如前一規則所指定。
  3. 註解可以擴充到任何類型的樹狀結構,而編譯器會以透明的方式用巨集的輸出樹狀結構取代輸入樹狀結構。
  4. 如果類別擴充到同名的類別和模組,則它們會成為伴隨物件。這樣一來,即使沒有明確宣告伴隨物件,也可以為類別產生伴隨物件。
  5. 頂層擴充必須保留註解的數量、類型和名稱,唯一的例外是類別可能會擴充到同名的類別加上同名的模組,在這種情況下,它們會自動成為伴隨物件,如前一規則所述。

以下是 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 巨集都與型別檢查器整合,而巨集標記也不例外。在擴充期間,我們可以取得所有可能的型別資訊(例如,我們可以針對周圍程式進行反映,或在封閉環境中執行型別檢查/隱式查詢)。

黑盒與白盒

巨集標記必須為 白盒。如果您將巨集標記宣告為 黑盒,它將無法運作。

此頁面的貢獻者