Макросы Scala 3: «динамическое» создание экземпляров одноэлементных объектов во время компиляции

Я пытаюсь создать макрос, использующий некоторые 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).

заранее спасибо

Попробуйте сделать Foo extend Singleton, который заставит всех клиентов использовать object. Хотя не уверен, что это поможет с макросами. - Кстати, какой в ​​этом смысл, кроме простого вызова вручную doStuff?

Luis Miguel Mejía Suárez 03.07.2024 23:14

Хорошая идея с Singleton, спасибо. Он лучше передает мои намерения (хотя я не могу расширить его напрямую, это должен быть собственный тип). К сожалению, я не вижу, как это помогает с самим макросом (поскольку макрос может это понять даже без явного добавления этого к признаку).

ncreep 04.07.2024 00:26

Что касается того, почему я хочу вызвать doStuff, то фактические методы, которые у меня есть для этих объектов, инкапсулируют различные древовидные преобразования (экземпляры TreeMap), которые я хочу применить к коду, и я хочу, чтобы клиентский код мог выбирать, какие преобразования применять в данный момент. сайт вызова макроса.

ncreep 04.07.2024 00:28

Что-то вроде inline def runFoo[A <: Foo & Singleton](inline foo: A): Unit ? Имейте в виду, что вам все равно нужно получить тип, затем Symbol, превратить его в Class и, наконец, получить экземпляр с помощью отражения, одновременно проверяя, что класс доступен даже во время компиляции (это не будет, если пользователь определит это object в та же область, что и расширение макроса).

Mateusz Kubuszok 04.07.2024 09:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
63
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я делал это раньше. Этот подход требует нескольких условий:

  • вы должны убедиться, что пользователь действительно пройдет 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]
      }
    }
    

    и это не может быть реализовано как objects - даже если они являются синглтонами - содержат ссылки на включающие классы. Таким образом, это все равно потребует проверки внутри макроса и ошибки компиляции с сообщением, объясняющим, почему

  • в общем, если вы хотите получить доступ к 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 преобразуется в байт-код, и т. д.

Спасибо за подробное объяснение! Я действительно надеялся избежать размышлений. Я предполагал, что если я смогу статически вызвать метод объекта из макроса, я смогу заставить макрос распознать его без отражения. Я поиграюсь с кодом, который вы написали, и посмотрю, как он пойдет. Спасибо

ncreep 04.07.2024 12:14

К сожалению, нет, в Scala 2 был reify, который по сути позволял компилировать код внутри макроса и получать результат компиляции, но в Scala 3 ничего этого нет. @DmytroMitin сделал github.com/DmytroMitin/dotty-patched, что позволяет это, но это неофициальная версия компилятора, не существует официального способа получения вещей, которые не гарантированно присутствуют во время компиляции (в основном примитивы, параметры , Либо и кортежи примитивов, Опции...). Если вы хотите получить выражение одноэлементного типа, отличного от этих, вы обходите проверки компилятора.

Mateusz Kubuszok 04.07.2024 12:23

понятно, еще раз спасибо

ncreep 04.07.2024 13:54

Другие вопросы по теме