巨集
在開發巨集時,啟用
-Xcheck-macros
scalac 選項旗標,以進行額外的執行時期檢查。
多階段
引號表達式
Scala 3 中的多階段程式設計使用引號 '{..}
來延遲,即階段,執行程式碼,並使用串接 ${..}
來評估和插入程式碼到引號中。引號表達式被輸入為 Expr[T]
,其協變類型參數為 T
。使用這兩個概念,可以輕鬆撰寫靜態安全的程式碼產生器。下列範例顯示 $x^n$ 數學運算的簡陋實作。
import scala.quoted.*
def unrolledPowerCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
if n == 0 then '{ 1.0 }
else if n == 1 then x
else '{ $x * ${ unrolledPowerCode(x, n-1) } }
'{
val x = ...
${ unrolledPowerCode('{x}, 3) } // evaluates to: x * x * x
}
引號和串接是彼此的對偶。對於類型為 T
的任意表達式 x
,我們有 ${'{x}} = x
,而對於類型為 Expr[T]
的任意表達式 e
,我們有 '{${e}} = e
。
抽象類型
引號可以使用類型類別 Type[T]
來處理泛型和抽象類型。引用泛型或抽象類型 T
的引號需要在隱含範圍中提供給定的 Type[T]
。下列範例顯示如何使用內容約束 (: Type
) 來註解 T
,以提供隱含的 Type[T]
,或等效的 using Type[T]
參數。
import scala.quoted.*
def singletonListExpr[T: Type](x: Expr[T])(using Quotes): Expr[List[T]] =
'{ List[T]($x) } // generic T used within a quote
def emptyListExpr[T](using Type[T], Quotes): Expr[List[T]] =
'{ List.empty[T] } // generic T used within a quote
如果找不到其他實例,預設會使用 Type.of[T]
。下列範例隱含地使用 Type.of[String]
和 Type.of[Option[U]]
。
val list1: Expr[List[String]] =
singletonListExpr('{"hello"}) // requires a given `Type[Sting]`
val list0: Expr[List[Option[T]]] =
emptyListExpr[Option[U]] // requires a given `Type[Option[U]]`
Type.of[T]
方法是編譯器會特別處理的基本運算。如果類型 T
是靜態已知的,或如果 T
包含我們有隱含 Type[Ui]
的其他類型 Ui
,它會提供隱含。在範例中,Type.of[String]
有靜態已知的類型,而 Type.of[Option[U]]
需要範圍內的隱含 Type[U]
。
引號內容
我們也使用給定的 Quotes
實例追蹤目前的引號內容。要建立引號 '{..}
,我們需要給定的 Quotes
內容,應該傳遞為函式的內容參數 (using Quotes)
。每個拼接都會在拼接範圍內提供新的 Quotes
內容。因此,引號和拼接可以視為具有下列簽章的方法,但具有特殊語意。
def '[T](x: T): Quotes ?=> Expr[T] // def '[T](x: T)(using Quotes): Expr[T]
def $[T](x: Quotes ?=> Expr[T]): T
具有問號 ?=>
的 lambda 是內容函式;它是一個 lambda,隱含地取得其引數並在 lambda 的實作中隱含地提供它。Quotes
用於各種用途,我們會在涵蓋這些主題時提到。
引號值
提升
雖然無法使用跨階段的本地變數持續性,但可以將它們提升到下一個階段。為此,我們提供 Expr.apply
方法,它可以取得一個值並將它提升到該值的引號表示法。
val expr1plus1: Expr[Int] = '{ 1 + 1 }
val expr2: Expr[Int] = Expr(1 + 1) // lift 2 into '{ 2 }
雖然它在類型上看起來類似於 '{ 1 + 1 }
,但 Expr(1 + 1)
的語意完全不同。Expr(1 + 1)
不會分階段或延遲任何運算;引數會評估為一個值,然後提升到引號中。引號將包含會在下一階段建立這個值的副本的程式碼。Expr
是多型的,而且可透過 ToExpr
類型類別由使用者擴充。
trait ToExpr[T]:
def apply(x: T)(using Quotes): Expr[T]
我們可以使用 given
定義實作 ToExpr
,它會將定義新增到範圍內的隱含定義中。在下列範例中,我們示範如何為任何可提升類型 `T` 實作 ToExpr[Option[T]]
。
given OptionToExpr[T: Type: ToExpr]: ToExpr[Option[T]] with
def apply(opt: Option[T])(using Quotes): Expr[Option[T]] =
opt match
case Some(x) => '{ Some[T]( ${Expr(x)} ) }
case None => '{ None }
ToExpr
對於基本類型必須實作為系統中的基本運算。在我們的案例中,我們使用反射 API 來實作它們。
從引號中萃取值
為了能夠使用 unrolledPowerCode
方法產生最佳化程式碼,巨集實作 powerCode
必須先判斷傳遞為參數 n
的引數是否為已知的常數值。這可以使用我們的程式庫實作中的 Expr.unapply
萃取器透過 unlifting 來達成,它只會在 n
是引號常數時比對並萃取出它的值。
def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
n match
case Expr(m) => // it is a constant: unlift code n='{m} into number m
unrolledPowerCode(x, m)
case _ => // not known: call power at run-time
'{ power($x, $n) }
或者,可以使用 n.value
方法來取得包含值的 Option[Int]
,或使用 n.valueOrAbort
直接取得值。
def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
// emits an error message if `n` is not a constant
unrolledPowerCode(x, n.valueOrAbort)
Expr.unapply
和 value
的所有變形都是多型且可透過給定的 FromExpr
類型類別由使用者擴充。
trait FromExpr[T]:
def unapply(x: Expr[T])(using Quotes): Option[T]
我們可以使用 given
定義來實作 FromExpr
,就像我們對 ToExpr
所做的一樣。FromExpr
對於基本類型必須實作為系統中的基本運算。在我們的案例中,我們使用反射 API 來實作它們。為了實作非基本類型的 FromExpr
,我們使用引號模式比對(例如 OptionFromExpr
)。
巨集和多階段程式設計
系統使用相同的引號抽象來支援多階段巨集和執行時期多階段程式設計。
多階段巨集
巨集
我們可以將拼接抽象化以表達巨集。巨集包含未巢狀在任何引號中的頂層拼接。在概念上,拼接的內容會比程式早一個階段評估。換句話說,內容會在編譯程式時評估。由巨集產生的產生程式碼會取代程式中的拼接。
def power2(x: Double): Double =
${ unrolledPowerCode('x, 2) } // x * x
內聯巨集
由於在程式中間使用拼接不像呼叫函式那麼符合人體工學;我們對巨集的最終使用者隱藏分段機制。我們有統一的方式來呼叫巨集和一般函式。為此,我們限制頂層拼接的使用,僅出現在內聯方法中[^1][^2]。
// inline macro definition
inline def powerMacro(x: Double, inline n: Int): Double =
${ powerCode('x, 'n) }
// user code
def power2(x: Double): Double =
powerMacro(x, 2) // x * x
巨集的評估只會在將程式內聯到 power2
時發生。內聯時,程式碼等同於 power2
先前的定義。使用內聯方法的後果是,巨集的引數和傳回類型都不必提到 Expr
類型;這對最終使用者隱藏了元程式設計的所有面向。
避免完整的詮釋器
在評估頂層拼接時,編譯器需要詮釋拼接中的程式碼。提供整個語言的詮釋器相當棘手,而且讓該詮釋器有效率地執行更是困難。為避免需要完整的詮釋器,我們可以對拼接施加下列限制,以簡化頂層拼接中程式碼的評估。
- 頂層拼接必須包含對已編譯靜態方法的單一呼叫。
- 函式的引數是文字常數、引號表達式(參數)、對類型參數的
Type.of
呼叫,以及對Quotes
的參考。
特別是,這些限制不允許在頂層拼接中使用拼接。這樣的拼接需要幾個解釋階段,這會不必要地降低效率。
編譯階段
巨集實作(即,在頂層拼接中呼叫的方法)可以來自任何預編譯函式庫。這在編譯程序的階段之間提供了明確的差異。考慮在不同函式庫中定義的下列 3 個原始檔。
// Macro.scala
def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] = ...
inline def powerMacro(x: Double, inline n: Int): Double =
${ powerCode('x, 'n) }
// Lib.scala (depends on Macro.scala)
def power2(x: Double) =
${ powerCode('x, '{2}) } // inlined from a call to: powerMacro(x, 2)
// App.scala (depends on Lib.scala)
@main def app() = power2(3.14)
一種語法上視覺化的方式是將應用程式放入一個引號中,以延遲編譯應用程式。然後,應用程式相依性可以放置在包含引號應用程式的外部引號中,並且我們對相依性的相依性重複此遞迴。
'{ // macro library (compilation stage 1)
def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
...
inline def powerMacro(x: Double, inline n: Int): Double =
${ powerCode('x, 'n) }
'{ // library using macros (compilation stage 2)
def power2(x: Double) =
${ powerCode('x, '{2}) } // inlined from a call to: powerMacro(x, 2)
'{ power2(3.14) /* app (compilation stage 3) */ }
}
}
為了讓系統更靈活,我們允許在定義巨集的專案中呼叫巨集,但有一些限制。例如,同時編譯 Macro.scala
和 Lib.scala
至同一個函式庫。為此,我們不遵循較簡單的語法模型,而是依賴原始檔的語意資訊。編譯原始檔時,如果我們偵測到呼叫尚未編譯的巨集,我們會將此原始檔的編譯延遲至下一個編譯階段。在範例中,我們會延遲編譯 Lib.scala
,因為它包含對 powerCode
的編譯時間呼叫。編譯階段會重複執行,直到所有原始檔都編譯完畢,或無法再進行。如果無法再進行,表示巨集的定義和使用之間存在循環相依性。我們還需要偵測在執行階段,巨集是否依賴尚未編譯的原始檔。偵測方式是執行巨集,並檢查 JVM 連結錯誤,以找出尚未編譯的類別。
執行階段多階段程式設計
請參閱 執行階段多階段程式設計
安全性
多階段程式設計在設計上是靜態安全且跨階段安全的。
靜態安全性
衛生
所有識別名稱都解釋為對引號中對應變數的符號參照。因此,在評估引號時,無法意外地將參照重新繫結到具有相同文字名稱的新變數。
類型良好
如果引號類型良好,則產生的程式碼類型良好。這是追蹤每個表達式類型的簡單結果。Expr[T]
只能從包含類型為 T
的表達式的引號建立。反之,Expr[T]
只能插入預期類型為 T
的位置。如前所述,Expr
在其類型參數中是協變的。這表示 Expr[T]
可以包含 T
子類型的表達式。當插入預期類型為 `T` 的位置時,這些表達式也具有有效的類型。
跨階段安全性
層級一致性
我們將某段程式碼的編寫層級定義為引號數減去包圍該程式碼的拼接數。區域變數必須在相同的編寫層級中定義和使用。
永遠無法從較低的編寫層級存取區域變數,因為它尚未存在。
def badPower(x: Double, n: Int): Double =
${ unrolledPowerCode('x, n) } // error: value of `n` not known yet
在巨集和跨平台可攜性的脈絡中,也就是說,在一個機器上編譯但可能在另一個機器上執行的巨集,我們無法支援區域變數的跨階段持久性。因此,區域變數只能在我們系統中完全相同的編寫層級中存取。
def badPowerCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
// error: `n` potentially not available in the next execution environment
'{ power($x, n) }
對於全域定義(例如 unrolledPowerCode
)的規則略有不同。可以產生包含對全域定義的參考的程式碼,例如 '{ power(2, 4) }
。這是一種有限形式的跨階段持久性,不會妨礙跨平台可攜性,其中我們參考 power
已編譯的程式碼。每個編譯步驟會將編寫層級降低一級,同時保留全域定義。因此,我們可以在巨集中參考編譯的定義,例如 ${ unrolledPowerCode('x, 2) }
中的 unrolledPowerCode
。
我們可以將層級一致性總結為兩條規則
- 區域變數只能在與其定義相同的編寫層級中使用
- 全域變數可以在任何編寫層級中使用
類型一致性
由於 Scala 使用類型擦除,因此泛型類型將在執行階段和任何後續階段中被擦除。為了確保任何引用泛型類型 T
的引號表達式不會遺失其需要的資訊,我們需要在範圍內給定 Type[T]
。Type[T]
會將未擦除的類型表示法傳遞到下一個階段。因此,在比其定義更高的編寫層級中使用的任何泛型類型都需要其 Type
。
範圍擠出
在接合的內容中,可以有一個引用,它指的是在外層引用中定義的局部變數。如果此引用用於接合中,則變數將在範圍內。但是,如果引用以某種方式擠出接合範圍之外,則變數可能不再在範圍內。可以透過可變狀態和例外等副作用擠出引用的表達式。以下範例顯示如何使用可變狀態擠出引用。
var x: Expr[T] = null
'{ (y: T) => ${ x = 'y; 1 } }
x // has value '{y} but y is not in scope
變數可以擠出的第二種方式是透過 run
方法。如果 run
使用引用的變數參考,它將不再在範圍內。結果將參考在下一階段定義的變數。
'{ (x: Int) => ${ run('x); ... } }
// evaluates to: '{ (x: Int) => ${ x; ... } 1
為了捕捉這兩種範圍擠出情況,我們的系統限制引用的使用,只有在引用未從接合範圍擠出時才允許接合引用。這與層級一致性不同,這是於執行階段[^4] 檢查,而不是編譯階段,以避免使靜態類型系統過於複雜。
每個 Quotes
執行個體包含一個唯一的範圍識別碼,並參考其父範圍,形成一個識別碼堆疊。Quotes
範圍的父範圍是 Quotes
的範圍,用於建立封閉引用。頂層接合和 run
建立新的範圍堆疊。每個 Expr
都知道它是在哪個範圍建立的。當它被接合時,我們會檢查引用範圍是否與接合範圍相同,或其父範圍。
分階段 Lambda
在函數語言中分階段執行程式時,有兩個基本的抽象:分階段 Lambda Expr[T => U]
和分階段 Lambda Expr[T] => Expr[U]
。第一個是在下一階段存在的函數,而第二個是在當前階段存在的函數。通常很方便有一個機制可以從 Expr[T => U]
轉換到 Expr[T] => Expr[U]
,反之亦然。
def later[T: Type, U: Type](f: Expr[T] => Expr[U]): Expr[T => U] =
'{ (x: T) => ${ f('x) } }
def now[T: Type, U: Type](f: Expr[T => U]): Expr[T] => Expr[U] =
(x: Expr[T]) => '{ $f($x) }
這兩種轉換都可以使用引號和拼接進行現成的執行。但如果 f
是已知的 lambda 函數,'{ $f($x) }
將不會就地 beta 縮減 lambda。此最佳化會在編譯器的後續階段執行。不立即縮減應用程式可以簡化已產生程式碼的分析。不過,可以使用 Expr.betaReduce
方法就地 beta 縮減 lambda。
def now[T: Type, U: Type](f: Expr[T => U]): Expr[T] => Expr[U] =
(x: Expr[T]) => Expr.betaReduce('{ $f($x) })
betaReduce
方法會盡可能 beta 縮減表達式的最外層應用程式(不論元數)。如果無法 beta 縮減表達式,則會傳回原始表達式。
分階段建構函式
要在後續階段建立新的類別實例,可以使用工廠方法(通常是 object
的 apply
方法)建立,或者可以使用 new
加以實例化。例如,我們可以撰寫 Some(1)
或 new Some(1)
,建立相同的值。在 Scala 3 中,如果找不到 apply
方法,使用工廠方法呼叫符號會回退到 new
。呼叫工廠方法時,我們遵循慣用的分階段規則。類似地,當我們使用 new C
時,會隱含呼叫 C
的建構函式,這也遵循慣用的分階段規則。因此,對於任意已知的類別 C
,我們可以使用 '{ C(...) }
或 '{ new C(...) }
作為建構函式。
分階段類別
引號程式碼可以包含任何有效的表達式,包括本機類別定義。這允許建立具有特殊化實作的新類別。例如,我們可以實作新版的 Runnable
,執行一些最佳化作業。
def mkRunnable(x: Int)(using Quotes): Expr[Runnable] = '{
class MyRunnable extends Runnable:
def run(): Unit = ... // generate some custom code that uses `x`
new MyRunnable
}
引號類別是本機類別,其類型無法跳脫封閉的引號。類別必須在引號內使用,或者可以使用已知介面(此案例中為 Runnable
)傳回其實例。
引號模式比對
有時需要分析程式碼結構或將程式碼分解成其子表達式。經典範例是嵌入式 DSL,其中巨集會知道一組定義,可以在編譯程式碼時重新詮釋這些定義(例如,執行最佳化)。在下列範例中,我們將 powCode
的前一個實作延伸,查看 x
以執行進一步的最佳化。
def fusedPowCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
x match
case '{ power($y, $m) } => // we have (y^m)^n
fusedPowCode(y, '{ $n * $m }) // generate code for y^(n*m)
case _ =>
'{ power($x, $n) }
子模式
在引號模式中,$
將子表達式繫結到表達式 Expr
,可以在該 case
分支中使用。引號模式中 ${..}
的內容是正規 Scala 模式。例如,我們可以在 ${..}
中使用 Expr(_)
模式,僅在它是已知值且提取它時才匹配。
def fusedUnrolledPowCode(x: Expr[Double], n: Int)(using Quotes): Expr[Double] =
x match
case '{ power($y, ${Expr(m)}) } => // we have (y^m)^n
fusedUnrolledPowCode(y, n * m) // generate code for y * ... * y
case _ => // ( n*m times )
unrolledPowerCode(x, n)
這些值提取子模式可以使用 FromExpr
的實例進行多型化。在以下範例中,我們展示 OptionFromExpr
的實作,它在內部使用 FromExpr[T]
來使用 Expr(x)
模式提取值。
given OptionFromExpr[T](using Type[T], FromExpr[T]): FromExpr[Option[T]] with
def unapply(x: Expr[Option[T]])(using Quotes): Option[Option[T]] =
x match
case '{ Some( ${Expr(x)} ) } => Some(Some(x))
case '{ None } => Some(None)
case _ => None
封閉模式
模式可能包含兩種參考:全域參考,例如 '{ power(...) }
中對 power
方法的呼叫,或對模式中定義的繫結的參考,例如 case '{ (x: Int) => x }
中的 x
。從引號中提取表達式時,我們需要確保不會從定義它的範圍中擠出任何變數。
'{ (x: Int) => x + 1 } match
case '{ (y: Int) => $z } =>
// should not match, otherwise: z = '{ x + 1 }
在此範例中,我們看到模式不應該匹配。否則,任何使用表達式 z
的情況都將包含對 x
的未繫結參考。為避免任何此類擠出,我們僅在表達式在模式內的定義下封閉時才匹配 ${..}
。因此,如果表達式未封閉,模式將不匹配。
HOAS 模式
為了允許提取可能包含擠出參考的表達式,我們提供一個高階抽象語法 (HOAS) 模式 $f(y)
(或 $f(y1,...,yn)
)。此模式將相對於 y
eta 展開子表達式,並將其繫結到 f
。lambda 參數將取代可能已被擠出的變數。
'{ ((x: Int) => x + 1).apply(2) } match
case '{ ((y: Int) => $f(y): Int).apply($z: Int) } =>
// f may contain references to `x` (replaced by `$y`)
// f = '{ (y: Int) => $y + 1 }
Expr.betaReduce('{ $f($z)}) // generates '{ 2 + 1 }
HOAS 樣式 $x(y1,...,yn)
僅會比對表達式,如果它不包含對樣式中定義變數的參照,而這些變數不在集合 y1,...,yn
中。換句話說,如果表達式僅包含對樣式中定義變數的參照,而這些變數在 y1,...,yn
中,則樣式會比對。請注意,HOAS 樣式 $x()
在語意上等同於封閉樣式 $x
。
類型變數
表達式可能包含靜態未知的類型。例如,Expr[List[Int]]
可能包含 list.map(_.toInt)
,其中 list
是某種類型的 List
。為了涵蓋所有可能的情況,我們需要對所有可能的類型(List[Int]
、List[Int => Int]
等)明確比對 list
。這是一個無限的類型集合,因此也是模式案例。即使我們知道特定程式可能使用的所有類型,我們最終仍可能得到無法管理的案例數目。為了克服這個問題,我們在引號樣式中引入類型變數,它將比對任何類型。
在以下範例中,我們展示類型變數 t
和 u
如何比對清單上連續呼叫 map
的所有可能配對。在引號樣式中,以小寫字母命名的類型會識別為類型變數。這遵循與一般樣式中使用的類型變數相同的表示法。
def fuseMapCode(x: Expr[List[Int]]): Expr[List[Int]] =
x match
case '{ ($ls: List[t]).map[u]($f).map[Int]($g) } =>
'{ $ls.map($g.compose($f)) }
...
fuseMapCode('{ List(1.2).map(f).map(g) }) // '{ List(1.2).map(g.compose(f)) }
fuseMapCode('{ List('a').map(h).map(i) }) // '{ List('a').map(i.compose(h)) }
變數 f
和 g
分別推斷為類型 Expr[t => u]
和 Expr[u => Int]
。隨後,我們可以推斷 $g.compose($f)
為類型 Expr[t => Int]
,這是 $ls.map(..)
參數的類型。
類型變數是會被抹除的抽象類型;這表示要參照它們,我們需要在第二個引號中給定 Type[t]
和 Type[u]
。引號樣式會隱含提供這些給定的類型。在執行階段,當樣式比對時,t
和 u
的類型會已知,而 Type[t]
和 Type[u]
會包含表達式中的精確類型。
由於 Expr
是協變的,表達式的靜態已知類型可能不是實際類型。類型變數也可以用於還原表達式的精確類型。
def let(x: Expr[Any])(using Quotes): Expr[Any] =
x match
case '{ $x: t } =>
'{ val y: t = $x; y }
let('{1}) // will return a `Expr[Any]` that contains an `Expr[Int]]`
也可以在樣式中多次參照同一個類型變數。
case '{ $x: (t, t) } =>
雖然我們可以在樣式的中間定義類型變數,但它們的正規形式是在樣式的開頭將它們定義為小寫名稱的 type
。
case '{ type t; $x: t } =>
這有點冗長,但有一些表達力優勢,例如允許定義變數的界限。
case '{ type t >: List[Int] <: Seq[Int]; $x: t } =>
類型樣式
只有一個類型而沒有該類型的表達式是可能的。為了能夠檢查類型,我們引入了引號類型樣式 case '[..] =>
。它的工作方式與引號樣式相同,但僅限於包含類型。類型變數可以用於引號類型樣式中以提取類型。
def empty[T: Type](using Quotes): Expr[T] =
Type.of[T] match
case '[String] => '{ "" }
case '[List[t]] => '{ List.empty[t] }
case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] }
...
Type.of[T]
用於在範圍內呼叫 Type[T]
的給定實例,它等於 summon[Type[T]]
。
可以使用類型變數上的適當類型界限來匹配高階類型。
def empty[K <: AnyKind : Type](using Quotes): Type[?] =
Type.of[K] match
case '[type f[X]; f] => Type.of[f]
case '[type f[X <: Int, Y]; f] => Type.of[f]
case '[type k <: AnyKind; k ] => Type.of[k]
類型測試和轉型
需要注意的是,對 Expr
的實例檢查和轉型,例如 isInstanceOf[Expr[T]]
和 asInstanceOf[Expr[T]]
,只會檢查實例是否屬於 Expr
類,但無法檢查 T
參數。這些情況會在編譯時發出警告,但如果被忽略,它們可能會導致意外的行為。
這些操作可以在系統中得到正確的支援。對於一個簡單的類型測試,可以使用 Expr
的 isExprOf[T]
方法來檢查它是否屬於該類型。類似地,可以使用 asExprOf[T]
將表達式轉型為給定的類型。這些操作使用給定的 Type[T]
來解決類型擦除問題。
子表達式轉換
此系統提供轉換表達式所有子表達式的機制。當我們想要轉換的子表達式在表達式中很深時,這很有用。如果表達式包含無法使用引號模式匹配的子表達式(例如,區域類別定義),這也是必要的。
trait ExprMap:
def transform[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T]
def transformChildren[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T] =
...
使用者可以延伸 ExprMap
特質並實作 transform
方法。此介面很彈性,可以實作由上而下、由下而上或其他轉換。
object OptimizeIdentity extends ExprMap:
def transform[T](e: Expr[T])(using Type[T])(using Quotes): Expr[T] =
transformChildren(e) match // bottom-up transformation
case '{ identity($x) } => x
case _ => e
transformChildren
方法實作為一個基本方法,知道如何到達所有直接子表達式,並對每個子表達式呼叫 transform
。傳遞給 transform
的類型是此子表達式在其表達式中的預期類型。例如,在 '{ val x: Option[Int] = Some(1); ...}
中轉換 Some(1)
時,類型將是 Option[Int]
,而不是 Some[Int]
。這表示我們可以安全地將 Some(1)
轉換為 None
。
分階段隱式召喚
使用 summon
召喚隱式引數時,我們會在目前的範圍中找到給定的實例。可以透過先明確分階段,使用 summon
來取得分階段隱式引數。在以下範例中,我們可以在巨集中傳遞一個隱式 Ordering[T]
作為 Expr[Ordering[T]]
給它的實作。然後,我們可以將其拼接並在下一階段隱式給予它。
inline def treeSetFor[T](using ord: Ordering[T]): Set[T] =
${ setExpr[T](using 'ord) }
def setExpr[T:Type](using ord: Expr[Ordering[T]])(using Quotes): Expr[Set[T]] =
'{ given Ordering[T] = $ord; new TreeSet[T]() }
我們將其作為隱式 Expr[Ordering[T]]
傳遞,因為可能有一些中間方法可以隱式傳遞它。
另一種方法是在呼叫巨集的範圍中召喚隱式值。使用 Expr.summon
方法,我們會取得包含隱式實例的選用表達式。這提供了有條件搜尋隱式實例的能力。
def summon[T: Type](using Quotes): Option[Expr[T]]
inline def setFor[T]: Set[T] =
${ setForExpr[T] }
def setForExpr[T: Type]()(using Quotes): Expr[Set[T]] =
Expr.summon[Ordering[T]] match
case Some(ord) =>
'{ new TreeSet[T]()($ord) }
case _ =>
'{ new HashSet[T] }
更多詳細資料
- 規格
- Scala 3 中的可擴充元程式設計[^1]
[^1]: Scala 3 中的可擴充元程式設計 [^2]: 語意保留元程式設計內聯 [^3]: 在 Scala 3 Dotty 專案中實作 https://github.com/lampepfl/dotty。sbt 函式庫相依性 "org.scala-lang" %% "scala3-staging" % scalaVersion.value
[^4]: 使用 -Xcheck-macros
編譯器旗標