執行階段多階段程式設計
此架構同時表達編譯階段元程式設計和多階段程式設計。我們可以將編譯階段元程式設計視為兩階段編譯程序:一個我們在頂層接合處撰寫程式碼,用於產生程式碼(巨集),另一個將在編譯階段執行所有必要的評估和一個我們將照常執行的物件程式。如果我們可以在執行階段合成程式碼並為程式設計師提供一個額外的階段,會如何?然後我們可以在執行階段擁有類型為 Expr[T]
的值,我們基本上可以將其視為類型化語法樹,我們可以將其顯示為字串(漂亮列印)或編譯並執行。如果引號數量超過接合處數量超過一個(實際上處理執行階段類型為 Expr[Expr[T]]
、Expr[Expr[Expr[T]]]
、... 的值),那麼我們稱之為多階段程式設計。
此範例背後的動機是讓執行階段資訊影響或引導程式碼產生。
直覺:執行程式碼的階段由其嵌入的接合處範圍和引號範圍之間的差異決定。
-
如果引號多於拼接,則程式碼會在編譯時執行,亦即作為巨集。一般來說,這表示執行一個解釋器,用來評估程式碼,而程式碼則表示為型別抽象語法樹。解釋器可以在評估先前編譯方法的應用程式時,回溯到反射呼叫。如果拼接過多,這表示巨集的實作程式碼(與它擴充的程式碼相反)會呼叫其他巨集。如果巨集是由解釋來實現,這將導致解釋器塔,其中第一個解釋器本身會解釋一個解釋器程式碼,而這個程式碼可能會解釋另一個解釋器,以此類推。
-
如果拼接數目等於引號數目,則程式碼會編譯並像往常一樣執行。
-
如果引號數目多於拼接數目,則程式碼會分階段執行。也就是說,它會在執行時產生型別抽象語法樹或型別結構。多於一個的引號過多對應於多階段程式設計。
提供一個用於完整語言的解釋器相當困難,而且要讓這個解釋器有效率地執行更困難。因此,我們目前對拼接的使用施加以下限制。
-
頂層拼接必須出現在內聯方法中(將該方法轉換成巨集)
-
拼接必須呼叫先前編譯的方法,傳遞引號引數、常數引數或內聯引數。
-
不允許拼接內部有拼接(但沒有介入的引號)。
API
到目前為止所討論的架構允許分段編寫程式碼,也就是準備在後續階段執行。若要執行該程式碼,Expr
類別中還有另一個稱為 run
的方法。請注意,$
和 run
都會將 Expr[T]
映射到 T
,但只有 $
會受到 跨階段安全性 的約束,而 run
只是個一般方法。scala.quoted.staging.run
提供一個 Quotes
,可用於在範圍內顯示該表達式。另一方面,scala.quoted.staging.withQuotes
提供一個 Quotes
,而不會評估表達式。
package scala.quoted.staging
def run[T](expr: Quotes ?=> Expr[T])(using Compiler): T = ...
def withQuotes[T](thunk: Quotes ?=> T)(using Compiler): T = ...
建立新的 Scala 3 專案並啟用分段編寫
sbt new scala/scala3-staging.g8
它會建立一個專案,其中包含必要的相依性項和一些範例。
如果您偏好自行建立專案,請務必在 build.sbt
建立定義 中定義下列相依性項
libraryDependencies += "org.scala-lang" %% "scala3-staging" % scalaVersion.value
如果您直接使用 scalac
/scala
,請同時使用 -with-compiler
旗標
scalac -with-compiler -d out Test.scala
scala -with-compiler -classpath out Test
範例
現在,採用與 巨集 中完全相同的範例。假設我們不想靜態傳遞陣列,而是在執行階段產生程式碼並傳遞值,也是在執行階段。請注意,我們如何在下方第 6 行建立型別為 Expr[Array[Int] => Int]
的未來階段函式。使用 staging.run { ... }
,我們可以在執行階段評估表達式。在 staging.run
的範圍內,我們也可以對表達式呼叫 show
,以取得表達式的類似來源的表示方式。
import scala.quoted.*
// make available the necessary compiler for runtime code generation
given staging.Compiler = staging.Compiler.make(getClass.getClassLoader)
val f: Array[Int] => Int = staging.run {
val stagedSum: Expr[Array[Int] => Int] =
'{ (arr: Array[Int]) => ${sum('arr)}}
println(stagedSum.show) // Prints "(arr: Array[Int]) => { var sum = 0; ... }"
stagedSum
}
f.apply(Array(1, 2, 3)) // Returns 6