使用Scala宏生成方法

时间:2015-10-22 11:02:48

标签: scala macros scala-macros scala-macro-paradise scala-meta

我想在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

另外请不要将此标记为this的副本,因为问题有点不同,过去3年Scala宏观区域的情况发生了很大变化。

1 个答案:

答案 0 :(得分:8)

这似乎与所述完全不同。在类成员上使用宏注释不允许您操作类本身的树。也就是说,当您使用宏注释在类中注释方法时,将调用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注释的方法,并创建多个别名树以拼接回到类中。如果没有带注释的方法,它只返回原始类树。因此,这不会检测重复的方法名称,并删除修饰符(编译器不会让我同时匹配注释和修饰符)。

这也很容易扩展到处理伴随对象,但是我把它们留下来以保持代码更小。有关我使用的匹配器,请参阅quasiquotes syntax summary。处理随播广告对象需要修改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