Reflection
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 можно найти в документации по API для scala.quoted.Quotes.reflectModule
.
Наиболее важным элементом на странице является дерево иерархии,
которое обеспечивает синтетический обзор отношений подтипов типов в API.
Для каждого типа Foo
в дереве:
- трейт
FooMethods
содержит методы, доступные для типаFoo
- трейт
FooModule
содержит статические методы, доступные для объектаFoo
. В частности, здесь находятся конструкторы (apply
/copy
) иunapply
метод, предоставляющий экстракторы, необходимые для сопоставления с образцом. - Для всех типов
Upper
таких какFoo <: Upper
, методы, определенные вUpperMethods
, также доступны дляFoo
Например, TypeBounds
, подтип TypeRepr
, представляет дерево типов в форме T >: L <: U
:
тип T
, который является надтипом L
и подтипом U
.
В TypeBoundsMethods
есть методы low
и hi
, которые позволяют получить доступ к представлениям L
и U
.
В 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 и Term
Выражения (Expr[T]
) можно рассматривать как обертки вокруг Term
, где T
статически известный тип термина.
Ниже используется метод расширения asTerm
для преобразования выражения в термин.
Этот метод расширения доступен только после импорта файлов quotes.reflect.asTerm
.
Затем используется asExprOf[Int]
, чтобы преобразовать термин обратно в Expr[Int]
.
Эта операция завершится ошибкой, если термин не имеет указанного типа (в данном случае Int
)
или если термин не является допустимым выражением.
Например, Ident(fn)
является недопустимым термином, если метод 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
Type и TypeRepr
Точно так же можно рассматривать Type[T]
как оболочку над TypeRepr
со статически известным типом T
.
Чтобы получить TypeRepr
, используется TypeRepr.of[T]
,
который ожидает given Type[T]
в области видимости (аналогично Type.of[T]
).
Также можно преобразовать его обратно в Type[?]
с помощью метода asType
.
Поскольку тип 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-интерфейсы Term
и TypeRepr
относительно закрыты в том смысле,
что методы производят и принимают значения, типы которых определены в API.
Однако можно заметить наличие Symbols
, которые идентифицируют определения.
И Term
, и TypeRepr
(и, следовательно, Expr
и Type
) имеют связанный символ.
Symbols
позволяют сравнить два определения по ==
, чтобы узнать, являются ли они одинаковыми.
Кроме того, Symbol
раскрывает и использует множество полезных методов.
Например:
declaredFields
иdeclaredMethods
позволяет перебирать поля и элементы, определенные внутри символаflags
позволяет проверить несколько свойств символаcompanionClass
иcompanionModule
предоставить способ перехода к сопутствующему объекту/классу и обратноTypeRepr.baseClasses
возвращает список символов родительских классов, расширенных типомSymbol.pos
дает доступ к положению, к исходному коду определения и даже к имени файла, в котором определен символ.- многие другие, которые можно найти в
SymbolMethods
К символу и обратно
Рассмотрим экземпляр типа TypeRepr
с именем val tpe: TypeRepr = ...
. Затем:
tpe.typeSymbol
возвращает символ типа, представленногоTypeRepr
. Рекомендуемый способ полученияSymbol
givenType[T]
-TypeRepr.of[T].typeSymbol
- Для одноэлементного типа
tpe.termSymbol
возвращает символ базового объекта или значения. tpe.memberType(symbol)
возвращаетTypeRepr
предоставленный символ- Для объектов
t: Tree
вызовt.symbol
возвращает символ, связанный с деревом. Учитывая, чтоTerm <: Tree
,Expr.asTerm.symbol
- это лучший способ получить символ, связанный сExpr[T]
- Для объектов
sym: Symbol
,sym.tree
возвращаетTree
, связанное с символом. Будьте осторожны при использовании этого метода, так как дерево для символа может быть не определено. Подробнее читайте на best practices page
API-дизайн макросов
Часто бывает полезно создавать вспомогательные методы или экстракторы, которые выполняют некоторую общую логику макросов.
Самыми простыми методами будут те, которые упоминают только Expr
, Type
и Quotes
в своей подписи.
Внутри они могут использовать отражение, но это не будет видно на месте использования метода.
def f(x: Expr[Int])(using Quotes): Expr[Int] =
import quotes.reflect.*
...
В некоторых случаях неизбежно, что некоторые методы будут ожидать
или возвращать Trees
или другие типы в quotes.reflect
.
В этих случаях рекомендуется следовать следующим примерам подписи метода:
Метод, который принимает quotes.reflect.Term
параметр
def f(using Quotes)(term: quotes.reflect.Term): String =
import quotes.reflect.*
...
Метод расширения для quotes.reflect.Term
возврата quotes.reflect.Tree
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
предоставляет набор полезных "принтеров" для отладки.
Примечательно, что классы TreeStructure
, TypeReprStructure
и ConstantStructure
могут быть весьма полезными.
Они будут печатать древовидную структуру в соответствии с экстракторами, которые потребуются для ее сопоставления.
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))
Таким образом, если case пропущен, ошибка сообщит о знакомой структуре, которую можно скопировать и вставить, чтобы устранить проблемы.
При желании можно сделать этот "принтер" "принтером" по умолчанию:
import quotes.reflect.*
given Printer[Tree] = Printer.TreeStructure
...
println(tree.show)
Ссылки: