2016-08-22 3 views
0

Примечание: есть EDIT внизу! Примечание: есть еще один EDIT внизу!Макрос аннотации Scala работает только с заранее определенными классами

Я написал макрос Scala annotation, который передается классу и создает (или, скорее, заполняет) объект case. Имя объекта case совпадает с именем переданного класса. Что еще более важно, для каждого поля пройденного класса будет поле в объекте с тем же именем. Однако поля объекта case имеют тип String, а их значение - имя типа соответствующего поля в переданном классе. Пример:

// Using the annotation macro to populate a case object called `String` 
@RegisterClass(classOf[String]) case object String 

// The class `String` defines a field called `value` of type `char[]`. 
// The case object also has a field `value`, containing `"char[]"`. 
println(String.value) // Prints `"char[]"` to the console 

Это, однако, кажется, работает только с заранее определенными классами, такими как String. Если я определяю case class A(...) и попытаться сделать @RegisterClass(classOf[A]) case object A, я получаю следующее сообщение об ошибке:

[info] scala.tools.reflect.ToolBoxError: reflective compilation has failed: 
[info] 
[info] not found: type A 

Что я сделал не так? Код моего макроса можно найти ниже. Кроме того, если кто-то замечает не-идиоматическую Скалу или плохие практики вообще, я бы не прочь намек. Заранее большое спасибо!

class RegisterClass[T](clazz: Class[T]) extends StaticAnnotation { 
    def macroTransform(annottees: Any*) = 
    macro RegisterClass.expandImpl[T] 
} 

object RegisterClass { 
    def expandImpl[T](c: blackbox.Context)(annottees: c.Expr[Any]*) = { 
    import c.universe._ 
    val clazz: Class[T] = c.prefix.tree match { 
     case q"new RegisterClass($clazz)" => c.eval[Class[T]](c.Expr(clazz)) 
     case _ => c.abort(c.enclosingPosition, "RegisterClass: Annotation expects a Class[T] instance as argument.") 
    } 
    annottees.map(_.tree) match { 
     case List(q"case object $caseObjectName") => 
     if (caseObjectName.toString != clazz.getSimpleName) 
      c.abort(c.enclosingPosition, "RegisterClass: Annotated case object and class T of passed Class[T] instance" + 
      "must have the same name.") 
     val clazzFields = clazz.getDeclaredFields.map(field => field.getName -> field.getType.getSimpleName).toList 
     val caseObjectFields = clazzFields.map(field => { 
      val fieldName: TermName = field._1 
      val fieldType: String = field._2 
      q"val $fieldName = $fieldType" 
     }) 
     c.Expr[Any](q"case object $caseObjectName { ..$caseObjectFields }") 
     case _ => c.abort(c.enclosingPosition, "RegisterClass: Annotation must be applied to a case object definition.") 
    } 
    } 
} 

EDIT: Как Евгений Burmako отметил, ошибка происходит потому, что class A не был составлен еще, так java.lang.Class для него не существует. Теперь я начал зарабатывать 100 очков StackOverflow для всех, кто как идея, как можно заставить это работать!

EDIT 2: Некоторая предыстория в прецеденте: как часть моего бакалавриата, я работаю над DSL Scala для выражения запросов для систем обработки событий. Эти запросы традиционно выражаются как строки, что вызывает множество проблем. Типичный запрос будет выглядеть так: «выберите A.id, B.timestamp из шаблона [A -> B]». Значение: Если происходит событие типа A, и после этого произойдет событие типа B, дайте мне id события A и timestamp события B. Типы A и B обычно представляют собой простые классы Java, над которыми я не имею контроля. id и timestamp - поля этих классов. Я бы хотел, чтобы мои DSL выглядели так: select (A.id, B.timestamp) { /* ... */}. Это означает, что для каждого класса, представляющего тип события, например, A, мне нужен объект-компаньон - в идеале с тем же именем. Этот объект-компаньон должен иметь те же поля, что и соответствующий класс, поэтому я могу передать свои поля функции select, например: select (A.id, B.timestamp) { /* ... */}. Таким образом, если я попытался передать A.idd функции select, во время компиляции это не сработало бы, если бы не было такого поля в исходном классе, потому что тогда не было бы и одного в сопутствующем объекте.

+0

Я не знаю, работает ли это на самом деле, но, возможно, это помогает: что, если вы добавите поле в объект case. I.e., '@RegisterClass case object String {type T = String}' или '@RegisterClass case object String {val t: String = _}'. Тогда, возможно, макрос аннотации может получить доступ к типу 'T' или типу' t' путем проверки аннотита? (Я не знаю, работает ли это, поэтому я не отправляю ответ, но, может быть, идея как-то помогает.) –

+0

Пара идей: (1) Переписывание объектов в таком большом масштабе - это плохая идея, поскольку вы по существу скрытие/перезапись существующей функциональности. (2) Spark 2.0 делает что-то подобное (но не проверяет тип, что было бы неплохо) с [столбцами] (http://spark.apache.org/docs/latest/api/scala/index.html#org.apache .spark.sql.Column) внутри операций, таких как 'select',' join', 'groupby' (см. примеры [здесь] (http://spark.apache.org/docs/latest/api/scala/index.html # org.apache.spark.sql.Dataset)). (3) Почему бы не сделать для этого плагин компилятора? Затем вы можете «переписать» синтаксис Scala локально. – Alec

ответ

1

Это не ответ на вашу проблему с макросом, но это может быть решением вашей общей проблемы.
Если вы можете разрешить незначительные изменения в синтаксисе вашего DSL, это может быть возможно без использования макросов (в зависимости от других требований, не упомянутых в этом вопросе).

scala> class Select[A,B]{ 
    | def apply[R,S](fa: A => R, fb: B => S)(body: => Unit) = ??? 
    | } 
defined class Select 

scala> def select[A,B] = new Select[A,B] 
select: [A, B]=> Select[A,B] 

scala> class MyA { def id = 42L } 
defined class MyA 

scala> class MyB { def timestamp = "foo" } 
defined class MyB 

scala> select[A,B](_.id, _.timestamp){ /* ... */ } 
scala.NotImplementedError: an implementation is missing 

Я использую класс Select здесь в качестве средства, чтобы иметь возможность указать типы ваших классов событий, позволяя компилятору вывести типы результирующих функций fa и fb.Если вам не нужны эти типы результатов, вы можете просто написать его как def select[A,B](fa: A => Any, fb: B => Any)(body: => Unit) = ???.

При необходимости вы можете реализовать метод select или apply в качестве макроса. Но, используя этот синтаксис, вам больше не нужно создавать объекты с помощью макрокоманд.

+0

Этот подход потрясающий! У меня есть еще один вопрос: какой способ Scala-esque реализовать это с помощью произвольного количества параметров для функции 'select'? Таким образом, вы также можете сделать это: 'select [A, B, C] (_. Foo, _.bar, _.baz)' – lambdarookie

+0

@scalarookie для этого вы действительно можете использовать макрос. Веселая. –