Можно ли избежать этой ошибки со свободным членом (возникающей при расширении макроса)?

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

Предположим, у нас есть это выражение:

val list = join {
  0
  1
  2
  3
}
println(list)

где join — это макрос, реализация которого:

def join(c: Ctx)(a: c.Expr[Int]): c.Expr[List[Int]] = {
  import c.mirror._
  a.tree match {
    case Block(list, ret) =>
      // c.reify(List(new c.Expr(list(0)).eval, 
      //              new c.Expr(list(1)).eval,
      //              new c.Expr(list(2)).eval) :+ new c.Expr(ret).eval)
      c.reify((for (expr <- list) yield new c.Expr(expr).eval) :+ new c.Expr(ret).eval)
  }
}

Цель макроса — объединить все элементы в блоке аргументов и вернуть их в один список. Поскольку содержимое блока может быть переменным, я не могу использовать закомментированное reify (что хорошо работает). Незакомментированный - с для понимания, который генерирует свободные термины - выдает сообщение:

"Расширение макроса содержит список переменных со свободным термином, определенный объединением в Macros.scala:48:18. Вы забыли использовать eval при вставке этой переменной в файл reifee? Если у вас возникли проблемы с отслеживанием переменных со свободным термином, рассмотрите возможность использования -Xlog -свободные условия"

Есть ли способ ввести понимание (или итератор или что-то еще), не получая этой ошибки? Кстати, я использую 2.10-M3.


person jeslg    schedule 14.06.2012    source источник


Ответы (1)


Проблема в том, что ваш код смешивает концепции времени компиляции и времени выполнения.

Используемая вами переменная «список» является значением времени компиляции (т. е. предполагается, что оно повторяется во время компиляции), и вы просите reify сохранить его до времени выполнения (путем объединения производных значений). Эта межстадийная головоломка приводит к созданию так называемого свободного термина.

Короче говоря, свободные термины — это заглушки, которые ссылаются на значения из более ранних этапов. Например, следующий фрагмент:

val x = 2
reify(x)

Будет компилироваться следующим образом:

val free$x1 = newFreeTerm("x", staticClass("scala.Int").asTypeConstructor, x);
Ident(free$x1)

Умно, да? Результат сохраняет тот факт, что x является Ident, сохраняет его тип (характеристика времени компиляции), но, тем не менее, ссылается и на свое значение (характеристика времени выполнения). Это стало возможным благодаря лексическому охвату.

Но если вы попытаетесь вернуть это дерево из расширения макроса (которое встроено в сайт вызова макроса), все взорвется. Сайт вызова макроса, скорее всего, не будет иметь x в своей лексической области, поэтому он не сможет ссылаться на значение x.

Что еще хуже. Если приведенный выше фрагмент написан внутри макроса, то x существует только во время компиляции, то есть в JVM, которая запускает компилятор. Но когда компилятор завершает работу, его нет.

Однако предполагается, что результаты расширения макроса, содержащие ссылку на x, должны выполняться во время выполнения (скорее всего, в другой JVM). Чтобы понять это, вам потребуется постоянство между этапами, то есть возможность каким-то образом сериализовать произвольные значения времени компиляции и десериализовать их во время выполнения. Я не знаю, как это сделать на скомпилированном языке, таком как Scala.


Обратите внимание, что в некоторых случаях возможна межэтапная персистентность. Например, если x был полем статического объекта:

object Foo { val x = 2 }
import Foo._
reify(x)

Тогда это не станет свободным термином, а будет прямо овеществлено:

Select(Ident(staticModule("Foo")), newTermName("x"))

Это интересная концепция, которая также обсуждалась в докладе SPJ на Scala Days 2012: http://skillsmatter.com/podcast/scala/haskell-cloud.

Чтобы убедиться, что какое-то выражение не содержит свободных терминов, в Haskell в компилятор добавляют новый встроенный примитив — конструктор типа Static. С макросами мы можем сделать это естественным образом, используя reify (который сам по себе является просто макросом). См. обсуждение здесь: https://groups.google.com/forum/#!topic/scala-internals/-42PWNkQJNA.


Хорошо, теперь мы увидели, в чем именно заключается проблема с исходным кодом, так как же заставить его работать?

К сожалению, нам придется вернуться к ручному построению AST, потому что reify испытывает трудности с выражением динамических деревьев. Идеальным вариантом использования reify в макрологии является наличие статического шаблона с типами дыр, известными во время компиляции макроса. Сделайте шаг в сторону - и вам придется прибегнуть к строительству деревьев вручную.

Суть в том, что вам нужно использовать следующее (работает с недавно выпущенной версией 2.10.0-M4, см. руководство по миграции на scala-language, чтобы узнать, что именно изменилось: http://groups.google.com/group/scala-language/browse_thread/thread/bf079865ad42249c):

import scala.reflect.makro.Context

object Macros {
  def join_impl(c: Context)(a: c.Expr[Int]): c.Expr[List[Int]] = {
    import c.universe._
    import definitions._
    a.tree match {
      case Block(list, ret) =>
        c.Expr((list :+ ret).foldRight(Ident(NilModule): Tree)((el, acc) => 
          Apply(Select(acc, newTermName("$colon$colon")), List(el))))
    }
  }

  def join(a: Int): List[Int] = macro join_impl
}
person Eugene Burmako    schedule 14.06.2012
comment
Будет ли что-то проще в scala 2.11? Помогут ли квазикотировки? (Я не изучал макросы глубоко, но всякий раз, когда я пытаюсь, я постоянно натыкаюсь на это) - person HRJ; 10.08.2013