Runtime Staging
Фреймворк одновременно выражает метапрограммирование времени компиляции и многоэтапное программирование.
Можно думать о метапрограммировании во время компиляции как о двухэтапном процессе компиляции:
на первом пишется код в сплайсах верхнего уровня, который будет использоваться для генерации кода (макросы),
и на втором выполняются все необходимые вычисления во время компиляции.
И объектная программа, которая будет запускаться как обычно.
Что, если бы можно было бы синтезировать код во время выполнения и предложить программисту ещё один дополнительный этап?
Затем может быть значение типа Expr[T]
во время выполнения,
которое по существу можно рассматривать как типизированное синтаксическое дерево,
доступное для показа в виде строки (красивая печать), либо скомпилировать и запустить.
Если количество цитат превышает количество вставок более чем на единицу
(эффективная обработка во время выполнения значений типа Expr[Expr[T]]
, Expr[Expr[Expr[T]]]
, ...),
то говорится о многоэтапном программировании.
Мотивация этой парадигмы состоит в том, чтобы позволить информации времени выполнения влиять на генерацию кода или направлять ее.
Интуиция: этап, на котором выполняется код, определяется разницей между количеством областей вставки и областей цитат, в которые он встроен.
- если сплайсов больше, чем кавычек, код запускается во время компиляции, т.е. как макрос. В общем случае это означает запуск интерпретатора, оценивающего код, представленный в виде типизированного абстрактного синтаксического дерева. Интерпретатор может вернуться к рефлексивным вызовам при оценке приложения ранее скомпилированного метода. Если превышение сплайсов больше одного, это будет означать, что код реализации макроса (в отличие от кода, в который он расширяется) вызывает другие макросы. Если макросы реализуются интерпретацией, это приведет к башням интерпретаторов, где первый интерпретатор сам будет интерпретировать код интерпретатора, который, возможно, интерпретирует другой интерпретатор и так далее.
- если количество сплайсов равно количеству кавычек, код компилируется и запускается как обычно.
- если количество кавычек превышает количество сплайсов, код поэтапный. То есть он создает типизированное абстрактное синтаксическое дерево или структуру типов во время выполнения. Превышение котировки более чем на единицу соответствует многоступенчатому программированию.
Предоставление интерпретатора для полного языка довольно сложно, и еще сложнее заставить этот интерпретатор работать эффективно. Поэтому в настоящее время накладываются следующие ограничения на использование сплайсов.
- сращивание верхнего уровня должно отображаться во встроенном методе (превращая этот метод в макрос).
- сращивание должно вызывать ранее скомпилированный метод, передавая аргументы в кавычках, константные аргументы или встроенные аргументы.
- сращивания внутри сплайсов (но без промежуточных кавычек) не допускаются.
API
Платформа позволяет выполнять код поэтапно, т.е. быть готовым к выполнению на более позднем этапе.
Для запуска этого кода в классе есть еще один метод в Expr
с именем run
.
Обратите внимание, что $
и run
обе преобразуют Expr[T]
в T
,
но только $
подпадает под действие PCP,
тогда как 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
Пример
Теперь возьмем точно такой же пример, как в макросах.
Предположим, что мы не хотим передавать массив статически,
а генерируем код во время выполнения и передаем значение также во время выполнения.
Обратите внимание, как создается функция будущей стадии типа Expr[Array[Int] => Int]
в строке 6.
С помощью 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
Ссылки: