scalabook

Форк
0
257 строк · 15.2 Кб

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
    given Type[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)

Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.