Как использовать тип, рассчитанный в макросе Scala, в предложении reify?

Я работаю с макросами Scala и имею следующий код в макросе:

    val fieldMemberType = fieldMember.typeSignatureIn(objectType) match {
      case NullaryMethodType(tpe)   => tpe
      case _                      => doesntCompile(s"$propertyName isn't a field, it must be another thing")
    }

    reify{
      new TypeBuilder() {
        type fieldType = fieldMemberType.type
      }
    }

Как видите, мне удалось получить c.universe.Type fieldMemberType. Это представляет тип определенного поля в объекте. Как только я это получу, я хочу создать новый объект TypeBuilder в файле reify. TypeBuilder — это абстрактный класс с абстрактным параметром. Этот абстрактный параметр — fieldType. Я хочу, чтобы этот fieldType был тем типом, который я нашел раньше.

Выполнение кода, показанного здесь, возвращает мне fieldMemberType not found. Есть ли способ заставить fieldMemberType работать внутри предложения reify?


person mgonto    schedule 10.12.2012    source источник


Ответы (2)


Проблема в том, что код, который вы передаете reify, по сути, будет дословно помещен в точку, где раскрывается макрос, а fieldMemberType там ничего не значит.

В некоторых случаях вы можете использовать splice, чтобы внедрить выражение, которое у вас есть во время макрорасширения, в код, который вы материализуете. Например, если бы мы пытались создать экземпляр этого трейта:

trait Foo { def i: Int }

И эта переменная была во время макрорасширения:

val myInt = 10

Мы могли бы написать следующее:

reify { new Foo { def i = c.literal(myInt).splice } }

Здесь это не сработает, а это значит, что вам придется забыть о милом маленьком reify и написать AST вручную. Вы обнаружите, что это случается часто, к сожалению. Мой стандартный подход — запустить новый REPL и ввести что-то вроде этого:

import scala.reflect.runtime.universe._

trait TypeBuilder { type fieldType }

showRaw(reify(new TypeBuilder { type fieldType = String }))

Это выдаст несколько строк AST, которые затем можно вырезать и вставить в определение макроса в качестве отправной точки. Затем вы возитесь с ним, заменяя такие вещи:

Ident(TypeBuilder)

С этим:

Ident(newTypeName("TypeBuilder"))

И FINAL с Flag.FINAL, и так далее. Я бы хотел, чтобы методы toString для типов AST более точно соответствовали коду, необходимому для их построения, но вы довольно быстро поймете, что вам нужно изменить. Вы получите что-то вроде этого:

c.Expr(
  Block(
    ClassDef(
      Modifiers(Flag.FINAL),
      anon,
      Nil,
      Template(
        Ident(newTypeName("TypeBuilder")) :: Nil,
        emptyValDef,
        List(
          constructor(c),
          TypeDef(
            Modifiers(),
            newTypeName("fieldType"),
            Nil,
            TypeTree(fieldMemberType)
          )
        )
      )
    ),
    Apply(Select(New(Ident(anon)), nme.CONSTRUCTOR), Nil)
  )
)

Где anon — это имя типа, которое вы создали заранее для своего анонимного класса, а constructor — это удобный метод, который я использую, чтобы сделать такие вещи немного менее отвратительными (вы можете найти его определение в конце полный рабочий пример).

Теперь, если мы завернем это выражение во что-то вроде это, мы можем написать следующее:

scala> TypeMemberExample.builderWithType[String]
res0: TypeBuilder{type fieldType = String} = $1$$1@fb3f1f3

Так что это работает. Мы взяли c.universe.Type (которое я получил здесь из WeakTypeTag параметра типа в builderWithType, но он будет работать точно так же с любым старым Type) и использовали его для определения члена типа нашего признака TypeBuilder.

person Travis Brown    schedule 10.12.2012
comment
Только один вопрос, как newTypeName находит Type? Я имею в виду, как он узнает, в каком пакете это находится? - person mgonto; 11.12.2012
comment
Спасибо! И newTypeName не выслеживает сам тип — он просто копирует строку, которую вы ей даете, в код, который генерирует макрос, который затем, как обычно, проверяется компилятором. Если вам нужно указать пакет, вы можете написать, например. Select(Ident("package"), newTypeName("MyClass")). - person Travis Brown; 11.12.2012
comment
Вы можете использовать -Yreify-copypaste и/или showRaw для получения кода, который создает интересующие вас фрагменты кода. - person Eugene Burmako; 12.12.2012
comment
Один комментарий по этому поводу: typeDef не работает, если у вас есть тип с общим типом в IT. Итак, в этом случае, если бы ваш fieldMemberType был Option[String], это вызвало бы исключение. Правильный способ сделать это — использовать TypeTree TypeTree (fieldMemberType). - person mgonto; 17.01.2013

Существует более простой подход, чем написание дерева для вашего варианта использования. На самом деле я использую его все время, чтобы держать деревья в страхе, так как программировать с деревьями может быть очень сложно. Я предпочитаю вычислять типы и использовать reify для создания деревьев. Это делает макросы более надежными и "гигиеничными" и снижает количество ошибок времени компиляции. IMO использование деревьев должно быть последним средством, только для нескольких случаев, таких как преобразование дерева или универсальное программирование для семейства типов, таких как кортежи.

Совет здесь состоит в том, чтобы определить функцию, принимающую в качестве параметров типа типы, которые вы хотите использовать в теле reify, с контекстом, привязанным к WeakTypeTag. Затем вы вызываете эту функцию, явно передавая WeakTypeTags, которые вы можете построить из типов юниверса благодаря контекстному методу WeakTypeTag.

Итак, в вашем случае это даст следующее.

  val fieldMemberType: Type = fieldMember.typeSignatureIn(objectType) match {
      case NullaryMethodType(tpe)   => tpe
      case _                      => doesntCompile(s"$propertyName isn't a field, it must be            another thing")
  }

  def genRes[T: WeakTypeTag] = reify{
    new TypeBuilder() {
      type fieldType = T
    }
  }

  genRes(c.WeakTypeTag(fieldMemberType))
person Leo    schedule 10.12.2012
comment
Это гораздо более чистый подход. Спасибо - person mgonto; 11.12.2012
comment
+1 (и я должен был заметить, что этот подход работает здесь), но по моему опыту есть много случаев (может быть, большинство), когда работа с деревьями напрямую неизбежна - в частности, когда вам нужно сослаться (или создать) методы или классы по их именам в виде строк. Например, здесь, здесь, здесь и т. д. - person Travis Brown; 11.12.2012