使用 Scala 宏生成方法 [英] Use Scala macros to generate methods

查看:34
本文介绍了使用 Scala 宏生成方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想在 Scala 2.11+ 中使用注释宏生成方法的别名.我什至不确定这是否可能.如果是,怎么做?

示例 - 鉴于以下情况,我希望注释宏扩展为

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

我希望上面生成的同义词方法存根如下:

类 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 元这可能吗?

注意:我的要求与:https://github.com/ktoso/scala-macro-method-alias

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

解决方案

这似乎不像所说的那样可能.在类成员上使用宏注释不允许您操作类本身的树.也就是说,当您使用宏注解对类中的方法进行注解时,将调用 macroTransform(annottees: Any*),但唯一的注解将是方法本身.

我能够获得一个使用两个注释的概念验证.这显然不如简单地注释类好,但我想不出另一种解决方法.

你需要:

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

想法是,你可以用这个注解对每个方法进行注解,这样父类上的宏注解就能找到你要扩展的方法.

class alias(aliases: String *) extends StaticAnnotation

然后是宏:

//注释包含类以扩展其中的别名方法@compileTimeOnly("你必须启用宏天堂插件.")类别名扩展 StaticAnnotation {def macroTransform(annottees: Any*): Any = 宏 AliasMacroImpl.impl}对象 AliasMacroImpl {def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = {导入 c.universe._val result = annottees map (_.tree) match {//匹配一个类,然后展开.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" <- statsLiteral(Constant(alias)) <- 别名ident = TermName(alias.toString)} 屈服 {val args = 参数映射 { paramList =>paramList.map { case q"$_ val $param: $_ = $_" =>q"$ 参数"}}q"def $ident[..$tparams](...$paramss): $tpt = $tname(...$args)"}if(aliasedDefs.nonEmpty) {问"""$mods 类 $tpname[..$tparams] $ctorMods(...$paramss) 扩展 { ..$earlydefns } with ..$parents { $self =>..$统计..$aliasedDefs}"""} 其他类定义//不是一个类.案例_=>c.abort(c.enclosingPosition, "无效的注解目标:不是一个类")}c.Expr[Any](结果)}}

请记住,这个实现会很脆弱.它只检查注释以检查第一个是 ClassDef.然后,它会查找类中使用 @alias 注释的方法的成员,并创建多个别名树以拼接回类中.如果没有注解的方法,它只返回原始的类树.照原样,这不会检测到重复的方法名称,并去除修饰符(编译器不会让我同时匹配注解和修饰符).

这也可以很容易地扩展为处理伴随对象,但我将它们省略以使代码更小.查看我使用的匹配器的 quasiquotes 语法摘要.处理伴随对象需要修改 result 匹配以处理 case classDef :: objDef :: Nil 和 case objDef :: Nil.p>

使用中:

@aliased类套接字 {@alias("问", "读")def load(n: Int): Seq[Byte] = Seq(1, 2, 3).map(_.toByte)}斯卡拉>val 套接字 = 新套接字套接字:套接字 = 套接字@7407d2b8斯卡拉>socket.load(5)res0: Seq[Byte] = List(1, 2, 3)斯卡拉>socket.ask(5)res1: Seq[Byte] = List(1, 2, 3)斯卡拉>socket.read(5)res2: Seq[Byte] = List(1, 2, 3)

它还可以处理多个参数列表:

@aliased类Foo {@alias("bar", "baz")def test(a: Int, b: Int)(c: String) = a + b + c}斯卡拉>val foo = 新的 Foo富:富=富@3857a375斯卡拉>foo.baz(1, 2)("4")res0: 字符串 = 34

I want to generate aliases of methods using annotation macros in Scala 2.11+. I am not even sure that is even possible. If yes, how?

Example - Given this below, I want the annotation macros to expand into

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

I want the above to generate the synonym method stubs as follows:

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)
}

The above is of course a facetious example but I can see this technique being useful to auto generate sync/async versions of APIs or in DSLs with lots of synonyms. Is it possible to also expose these generated methods in the Scaladoc too? Is this something possible using Scala meta?

Note: What I am asking is quite different from: https://github.com/ktoso/scala-macro-method-alias

Also please don't mark this as a duplicate of this as the question is a bit different and a lot has changed in Scala macro land in past 3 years.

解决方案

This doesn't seem possible exactly as stated. Using a macro annotation on a class member does not allow you to manipulate the tree of the class itself. That is, when you annotate a method within a class with a macro annotation, macroTransform(annottees: Any*) will be called, but the only annottee will be the method itself.

I was able to get a proof-of-concept working with two annotations. It's obviously not as nice as simply annotating the class, but I can't think of another way around it.

You'll need:

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

The idea is, you can annotate each method with this annotation, so that a macro annotation on the parent class is able to find which methods you want to expand.

class alias(aliases: String *) extends StaticAnnotation

Then the macro:

// 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)
  }

}

Keep in mind this implementation will be brittle. It only inspects the annottees to check that the first is a ClassDef. Then, it looks for members of the class that are methods annotated with @alias, and creates multiple aliased trees to splice back into the class. If there are no annotated methods, it simply returns the original class tree. As is, this will not detect duplicate method names, and strips away modifiers (the compiler would not let me match annotations and modifiers at the same time).

This can easily be expanded to handle companion objects as well, but I left them out to keep the code smaller. See the quasiquotes syntax summary for the matchers I used. Handling companion objects would require modifying the result match to handle case classDef :: objDef :: Nil, and case objDef :: Nil.

In use:

@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)

It can also handle multiple parameter lists:

@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

这篇关于使用 Scala 宏生成方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆