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 в дереве:

Например, 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 раскрывает и использует множество полезных методов. Например:

К символу и обратно

Рассмотрим экземпляр типа TypeRepr с именем val tpe: TypeRepr = .... Затем:

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)

Ссылки: