Scala 3 中的巨集

反射

語言
此文件頁面專門針對 Scala 3,並可能涵蓋 Scala 2 中沒有的新概念。除非另有說明,否則此頁面中的所有程式碼範例假設您使用的是 Scala 3。

反射 API 提供了更複雜且全面的程式碼結構檢視。它提供了型別抽象語法樹及其屬性的檢視,例如型別、符號、位置和註解。

API 可用於巨集中,也可用于 檢查 TASTy 檔案

如何使用 API

反射 API 在型別 Quotes 中定義為 reflect。實際執行個體取決於目前的範圍,其中使用引號或引號模式配對。因此,每個巨集方法都會收到 Quotes 作為額外的引數。由於 Quotes 是上下文相關的,因此要存取其成員,我們需要命名參數或呼叫它。標準函式庫中的下列定義詳細說明了存取它的正規方法

package scala.quoted

transparent inline def quotes(using inline q: Quotes): q.type = q

我們可以使用 scala.quoted.quotes 來匯入範圍內目前的 Quotes

import scala.quoted.* // Import `quotes`, `Quotes`, and `Expr`

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.* // Import `Tree`, `TypeRepr`, `Symbol`, `Position`, .....
  val tree: Tree = ...
  ...

這將匯入 API 的所有類型和模組(包含擴充方法)。

如何瀏覽 API

完整的 API 可以從 scala.quoted.Quotes.reflectModule 的 API 文件 中找到。遺憾的是,在這個階段,這個自動產生的文件並不容易瀏覽。

頁面上最重要的元素是階層樹,它提供了 API 中類型的子類型關係的綜合概觀。對於樹中的每個類型 Foo

  • 特徵 FooMethods 包含類型 Foo 上可用的方法
  • 特徵 FooModule 包含物件 Foo 上可用的靜態方法。最值得注意的是,建構函式(apply/copy)和 unapply 方法,它提供了模式比對所需的萃取器
  • 對於所有類型 Upper,使得 Foo <: Upper,在 UpperMethods 中定義的方法也可用於 Foo

例如,TypeBoundsTypeRepr 的子類型,表示形式為 T >: L <: U 的類型樹:類型 TL 的超類型,也是 U 的子類型。在 TypeBoundsMethods 中,您將找到方法 lowhi,它們允許您存取 LU 的表示。在 TypeBoundsModule 中,您將找到 unapply 方法,它允許您撰寫

def f(tpe: TypeRepr) =
  tpe match 
    case TypeBounds(l, u) =>

因為 TypeBounds <: TypeRepr,所以 TypeReprMethods 中定義的所有方法都可用於 TypeBounds

def f(tpe: TypeRepr) =
  tpe match
    case tpe: TypeBounds =>
      val low = tpe.low
      val hi  = tpe.hi

與 Expr/Type 的關係

Expr 和 Term

表達式 (Expr[T]) 可以視為 Term 的包裝器,其中 T 是該項目的靜態已知類型。以下,我們使用擴充方法 asTerm 將表達式轉換為項目。此擴充方法僅在匯入 quotes.reflect.asTerm 後才可用。接著我們使用 asExprOf[Int] 將項目轉換回 Expr[Int]。如果項目的類型不符 (在此情況下為 Int) 或項目不是有效的表達式,此操作將會失敗。例如,如果方法 fn 採用類型參數,則 Ident(fn) 會是無效的項目,在這種情況下我們需要 Apply(Ident(fn), args)

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.*
  val tree: Term = x.asTerm
  val expr: Expr[Int] = tree.asExprOf[Int]
  expr

類型和 TypeRepr

類似地,我們也可以將 Type[T] 視為 TypeRepr 的包裝器,其中 T 是靜態已知類型。若要取得 TypeRepr,我們使用 TypeRepr.of[T],它預期在範圍內有指定的 Type[T] (類似於 Type.of[T])。我們也可以使用 asType 方法將其轉換回 Type[?]。由於 Type[?] 的類型並非靜態已知,因此我們需要使用存在類型為其命名才能使用它。這可以使用 '[t] 模式來達成。

def g[T: Type](using Quotes) =
  import quotes.reflect.*
  val tpe: TypeRepr = TypeRepr.of[T]
  tpe.asType match
    case '[t] => '{ val x: t = ${...} }
  ...

符號

就方法產生並接受其類型在 API 中定義的值這一點而言,TermTypeRepr 的 API 相對「封閉」。不過,你可能會注意到有識別定義的 Symbol

無論是 TermTypeRepr (因此也包括 ExprType),都有一個關聯的符號。透過使用 ==Symbol 可以比較兩個定義是否相同。此外,Symbol 會公開許多有用的方法,並由這些方法使用。例如

  • declaredFieldsdeclaredMethods 允許您在符號內定義的欄位和成員上進行反覆運算
  • flags 允許您檢查符號的多個屬性
  • companionClasscompanionModule 提供一種跳轉至伴隨物件/類別和從中跳出的方式
  • TypeRepr.baseClasses 傳回由類別延伸的符號清單
  • Symbol.pos 讓您可以存取定義符號的位置、定義的原始程式碼,甚至是定義符號的檔案名稱
  • 您可以在 SymbolMethods 中找到許多其他方法

轉換為 Symbol 和返回

考慮一個名為 val tpe: TypeRepr = ...TypeRepr 類型的實例。然後

  • tpe.typeSymbol 傳回 TypeRepr 所表示類型的符號。在給定 Type[T] 的情況下取得 Symbol 的建議方式為 TypeRepr.of[T].typeSymbol
  • 對於單例類型,tpe.termSymbol 傳回基礎物件或值的符號
  • tpe.memberType(symbol) 傳回所提供符號的 TypeRepr
  • 在物件 t: Tree 上,t.symbol 傳回與樹狀結構相關聯的符號。由於 Term <: TreeExpr.asTerm.symbol 是取得與 Expr[T] 相關聯的符號的最佳方式
  • 在物件 sym: Symbol 上,sym.tree 傳回與符號相關聯的 Tree。使用此方法時請小心,因為符號的樹狀結構可能未定義。請閱讀 最佳實務頁面 以取得更多資訊

巨集 API 設計

建立執行巨集一些常見邏輯的輔助方法或萃取器通常會很有用。

最簡單的方法是僅在簽章中提及 ExprTypeQuotes。在內部,它們可能會使用反射,但這不會在方法的使用位置看到。

def f(x: Expr[Int])(using Quotes): Expr[Int] =
  import quotes.reflect.*
  ...

在某些情況下,某些方法可能會預期或傳回 Treequotes.reflect 中的其他類型,這是不可避免的。對於這些情況,最佳做法是遵循下列方法簽章範例

採用 quotes.reflect.Term 參數的方法

def f(using Quotes)(term: quotes.reflect.Term): String =
  import quotes.reflect.*
  ...

傳回 quotes.reflect.Treequotes.reflect.Term 延伸方法

extension (using Quotes)(term: quotes.reflect.Term)
  def g: quotes.reflect.Tree = ...

quotes.reflect.Term 相符的萃取器

object MyExtractor:
  def unapply(using Quotes)(x: quotes.reflect.Term) =
    ...
    Some(y)

避免將 Quotes 內容儲存在欄位中。欄位中的 Quotes 必然會因為造成涉及具有不同路徑的 Quotes 的錯誤而讓其使用更為困難。

通常,這些模式已在使用 Scala 2 定義延伸方法或情境式 unapply 的程式碼中看到。現在我們有可在其他參數之前加入的 given 參數,所有這些舊的解決方法都不再需要。新的抽象化讓定義位置和使用位置都更為簡單。

偵錯

執行時期檢查

表達式 (Expr[T]) 可視為 Term 的包裝器,其中 T 是該項目的靜態已知類型。因此,這些檢查將在執行時期執行 (即巨集展開時的編譯時期)。

建議在開發巨集或巨集測試時啟用 -Xcheck-macros 旗標。此旗標將啟用額外的執行時期檢查,這些檢查會嘗試在建立樹狀結構或類型時立即找出格式不正確的樹狀結構或類型。

還有 -Ycheck:all 旗標,它會檢查樹狀結構格式正確性的所有編譯器不變式。這些檢查通常會因斷言錯誤而失敗。

列印樹狀結構

toString 方法在 quotes.reflect 套件中的類型中並非用於除錯,因為它們顯示的是內部表示,而非 quotes.reflect 表示。在許多情況下,這些表示是相似的,但有時它們可能會誤導除錯程序,因此不應依賴它們。

相反地,quotes.reflect.Printers 提供了一組有用的印表機,用於除錯。特別是 TreeStructureTypeReprStructureConstantStructure 類別可能非常有用。這些類別將印出樹狀結構,大致遵循匹配它所需的萃取器。

val tree: Tree = ...
println(tree.show(using Printer.TreeStructure))

可以新增此功能的最有用地方之一是在 Tree 上的樣式比對結束時。

tree match
  case Ident(_) =>
  case Select(_, _) =>
  ...
  case _ =>
    throw new MatchError(tree.show(using Printer.TreeStructure))

這樣一來,如果錯過某個情況,錯誤將會報告一個熟悉的結構,可以複製貼上以開始修正問題。

如果需要,您可以將此印表機設為預設值

  import quotes.reflect.*
  given Printer[Tree] = Printer.TreeStructure
  ...
  println(tree.show)

更多

即將推出

此頁面的貢獻者