Я пытаюсь создать макрос, использующий некоторые object
.
Предположим, у меня есть следующие определения:
trait Foo:
def doStuff(): Unit
// in other files
object Bar extends Foo:
def doStuff() = ...
object Qux extends Foo:
def doStuff() = ...
(Foo
специально не запечатано, см. ниже)
Я хочу создать макрос, который имеет следующую форму:
inline def runFoo(inline foo: Foo): Unit = ${ runFooImpl('foo) }
Таким образом, при вызове runFoo
с любым одноэлементным экземпляром Foo
соответствующий метод doStuff
будет вызываться во время компиляции.
Например:
runFoo(Bar)
Запустит Bar.doStuff()
во время компиляции.
В псевдокоде макрос будет выглядеть примерно так:
def runFooImpl(fooExpr: Expr[Foo]): Expr[Unit] =
val foo: Foo = fooExpr.valueOrAbort // does not compile
foo.doStuff()
'{ () }
В настоящее время valueOrAbort
не может работать из-за отсутствия FromExpr
.
Мой вопрос: есть ли какой-нибудь способ использовать тот факт, что Bar
, Qux
и т. д. являются константами времени компиляции, чтобы можно было извлечь их из конкретного Expr[Foo]
во время расширения макроса?
Обратите внимание, что превращение Foo
в запечатанный признак (и запись FromExpr
путем сопоставления с образцом) не является приемлемым решением, поскольку я хочу, чтобы Foo
расширялся с помощью клиентского кода (с ограничением, согласно которому все реализации Foo
должны быть object
).
заранее спасибо
Хорошая идея с Singleton
, спасибо. Он лучше передает мои намерения (хотя я не могу расширить его напрямую, это должен быть собственный тип). К сожалению, я не вижу, как это помогает с самим макросом (поскольку макрос может это понять даже без явного добавления этого к признаку).
Что касается того, почему я хочу вызвать doStuff
, то фактические методы, которые у меня есть для этих объектов, инкапсулируют различные древовидные преобразования (экземпляры TreeMap
), которые я хочу применить к коду, и я хочу, чтобы клиентский код мог выбирать, какие преобразования применять в данный момент. сайт вызова макроса.
Что-то вроде inline def runFoo[A <: Foo & Singleton](inline foo: A): Unit
? Имейте в виду, что вам все равно нужно получить тип, затем Symbol
, превратить его в Class
и, наконец, получить экземпляр с помощью отражения, одновременно проверяя, что класс доступен даже во время компиляции (это не будет, если пользователь определит это object
в та же область, что и расширение макроса).
Я делал это раньше. Этот подход требует нескольких условий:
вы должны убедиться, что пользователь действительно пройдет object
- вы можете сделать это, например, inline def runFoo[A <: Foo & Singleton]: Unit
вы НЕ МОЖЕТЕ запретить пользователю делать что-то вроде
class Example1 {
def localFunction: Unit = {
object LocalDefinition extends Foo
runFoo[LocalDefinition]
}
}
class Example2 {
object LocalDefinition extends Foo
def localFunction: Unit = {
runFoo[LocalDefinition]
}
}
и это не может быть реализовано как object
s - даже если они являются синглтонами - содержат ссылки на включающие классы. Таким образом, это все равно потребует проверки внутри макроса и ошибки компиляции с сообщением, объясняющим, почему
в общем, если вы хотите получить доступ к object A extends Foo
, в пути к классам должен быть доступен Class[A$]
, доступный для макроса, поэтому даже такие вещи, как
object LocalDefinition extends Foo
runFoo[LocalDefinition]
запрещено, так как вы не можете получить экземпляр чего-то, что еще не выпустило байт-код (а это не так, поскольку файл все еще компилируется, о чем свидетельствует продолжающееся расширение макроса)
Как только мы примем эти ограничения, мы сможем вместе что-нибудь взломать. Я уже создавал прототип чего-то подобного несколько лет назад и использовал результаты в своей OSS-библиотеке, чтобы позволить пользователям настраивать, как должно работать сравнение строк.
Вы начинаете с создания точки входа в макрос:
object Macro:
inline def runStuff[A <: Foo & Singleton](inline a: A): Unit =
${ runStuffImpl[A] }
import scala.quoted.*
def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] =
???
Затем мы могли бы реализовать фрагмент кода, который преобразует Symbol
имя в Class
имя. Я буду использовать упрощенную версию, которая не обрабатывает вложенные объекты:
def summonModule[M <: Singleton: Type](using Quotes): Option[M] =
val name: String = TypeRepr.of[M].typeSymbol.companionModule.fullName
val fixedName = name.replace(raw"$$.", raw"$$") + "$"
try
Option(Class.forName(fixedName).getField("MODULE$").get(null).asInstanceOf[M])
catch
case _: Throwable => None
с этим мы действительно можем использовать реализацию в макросе (если она доступна):
def runStuffImpl[A <: Foo & Singleton: Type](using Quotes): Expr[Unit] = {
import quotes.*
summonModule[A] match
case Some(foo) =>
foo.doStuff()
'{ () }
case None =>
reflect.report.throwError(s"${TypeRepr.of[A].show} cannot be used in macros")
}
Сделанный.
Тем не менее, этот шаблон класса типов макроса, как я бы его назвал, довольно хрупкий, подвержен ошибкам и неинтуитивен для пользователя, поэтому я бы посоветовал не использовать его, если это возможно, и четко объяснить, какие объекты могут туда помещаться. , как в документации, так и в сообщении об ошибке. Даже тогда это была бы в значительной степени проклятая функция.
Я бы также рекомендовал воздержаться от этого, если вы не можете понять, почему это работает, прочитав код - было бы довольно сложно исправить/редактировать/отладить это, если вы не можете разобраться в путях к классам, загрузчиках классов, предварительном просмотре того, как код Scala преобразуется в байт-код, и т. д.
Спасибо за подробное объяснение! Я действительно надеялся избежать размышлений. Я предполагал, что если я смогу статически вызвать метод объекта из макроса, я смогу заставить макрос распознать его без отражения. Я поиграюсь с кодом, который вы написали, и посмотрю, как он пойдет. Спасибо
К сожалению, нет, в Scala 2 был reify
, который по сути позволял компилировать код внутри макроса и получать результат компиляции, но в Scala 3 ничего этого нет. @DmytroMitin сделал github.com/DmytroMitin/dotty-patched, что позволяет это, но это неофициальная версия компилятора, не существует официального способа получения вещей, которые не гарантированно присутствуют во время компиляции (в основном примитивы, параметры , Либо и кортежи примитивов, Опции...). Если вы хотите получить выражение одноэлементного типа, отличного от этих, вы обходите проверки компилятора.
понятно, еще раз спасибо
Попробуйте сделать
Foo extend Singleton
, который заставит всех клиентов использоватьobject
. Хотя не уверен, что это поможет с макросами. - Кстати, какой в этом смысл, кроме простого вызова вручнуюdoStuff
?