Используйте макросы Scala для генерации методов

Я хочу генерировать псевдонимы методов, используя макросы аннотаций в Scala 2.11+. Я даже не уверен, что это возможно. Если да, то как?

Пример. Учитывая это ниже, я хочу, чтобы макросы аннотаций расширились до

class Socket {
  @alias(aliases = Seq("!", "ask", "read"))
  def load(n: Int): Seq[Byte] = {/* impl */}
}

Я хочу, чтобы выше, чтобы генерировать заглушки метода синонимов следующим образом:

class Socket {
  def load(n: Int): Seq[Byte] = // .... 
  def !(n: Int) = load(n)
  def ask(n: Int) = load(n)
  def read(n: Int) = load(n)
}

Вышеприведенный пример, конечно, показателен, но я вижу, что этот метод полезен для автоматической генерации синхронизирующих / асинхронных версий API или в DSL с большим количеством синонимов. Можно ли также представить эти сгенерированные методы в Scaladoc? Возможно ли это с помощью Scala meta?

Примечание: то, что я спрашиваю, сильно отличается от: https://github.com/ktoso/scala-macro-method-alias

Также, пожалуйста, не помечайте это как дубликат этого вопроса, так как вопрос немного другой, и многое изменилось в макрополе Scala за последние 3 года.

1 ответ

Решение

Это не представляется возможным в точности как заявлено. Использование макросовой аннотации для члена класса не позволяет вам манипулировать деревом самого класса. То есть, когда вы аннотируете метод в классе с помощью макро-аннотации, macroTransform(annottees: Any*) будет вызван, но единственным аннотируемым будет сам метод.

Я смог получить подтверждение концепции, работая с двумя аннотациями. Очевидно, это не так хорошо, как просто аннотировать класс, но я не могу придумать другого пути.

Тебе понадобиться:

import scala.annotation.{ StaticAnnotation, compileTimeOnly }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

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

class alias(aliases: String *) extends StaticAnnotation

Тогда макрос:

// Annotate the containing class to expand aliased methods within
@compileTimeOnly("You must enable the macro paradise plugin.")
class aliased extends StaticAnnotation {
    def macroTransform(annottees: Any*): Any = macro AliasMacroImpl.impl
}

object AliasMacroImpl {

  def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {
    import c.universe._

    val result = annottees map (_.tree) match {
      // Match a class, and expand.
      case (classDef @ q"$mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self => ..$stats }") :: _ =>

        val aliasedDefs = for {
          q"@alias(..$aliases) def $tname[..$tparams](...$paramss): $tpt = $expr" <- stats
          Literal(Constant(alias)) <- aliases
          ident = TermName(alias.toString)
        } yield {
          val args = paramss map { paramList =>
            paramList.map { case q"$_ val $param: $_ = $_" => q"$param" }
          }

          q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$args)"
        }

        if(aliasedDefs.nonEmpty) {
          q"""
            $mods class $tpname[..$tparams] $ctorMods(...$paramss) extends { ..$earlydefns } with ..$parents { $self =>
              ..$stats
              ..$aliasedDefs
            }
          """
        } else classDef
        // Not a class.
        case _ => c.abort(c.enclosingPosition, "Invalid annotation target: not a class")
    }

    c.Expr[Any](result)
  }

}

Имейте в виду, что эта реализация будет хрупкой. Он только осматривает аннотируемых, чтобы убедиться, что первый ClassDef, Затем он ищет членов класса, которые являются методами, аннотированными @aliasи создает несколько псевдонимов для объединения в класс. Если нет аннотированных методов, он просто возвращает исходное дерево классов. Таким образом, это не обнаружит дубликаты имен методов и удалит модификаторы (компилятор не позволит мне сопоставлять аннотации и модификаторы одновременно).

Это также может быть легко расширено для обработки сопутствующих объектов, но я оставил их, чтобы уменьшить размер кода. Смотрите сводку синтаксиса квазицитатов для использованных мной соответствий. Обработка сопутствующих объектов потребует изменения result соответствовать, чтобы обращаться case classDef :: objDef :: Nilи случай objDef :: Nil,

В использовании:

@aliased
class Socket {
    @alias("ask", "read")
    def load(n: Int): Seq[Byte] = Seq(1, 2, 3).map(_.toByte)
}

scala> val socket = new Socket
socket: Socket = Socket@7407d2b8

scala> socket.load(5)
res0: Seq[Byte] = List(1, 2, 3)

scala> socket.ask(5)
res1: Seq[Byte] = List(1, 2, 3)

scala> socket.read(5)
res2: Seq[Byte] = List(1, 2, 3)

Он также может обрабатывать несколько списков параметров:

@aliased
class Foo {
    @alias("bar", "baz")
    def test(a: Int, b: Int)(c: String) = a + b + c
}

scala> val foo = new Foo
foo: Foo = Foo@3857a375

scala> foo.baz(1, 2)("4")
res0: String = 34
Другие вопросы по тегам