运行时多阶段编程
该框架同时表达了编译时元编程和多阶段编程。我们可以将编译时元编程视为一个两阶段编译过程:一个我们用顶层拼接编写代码的过程,该过程将用于代码生成(宏),另一个将在编译时执行所有必要的评估,以及一个我们通常会运行的对象程序。如果我们可以在运行时合成代码并为程序员提供一个额外的阶段,会怎样?然后,我们可以在运行时获得一个类型为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