使用 Scala 宏生成方法 [英] Use Scala macros to generate methods
问题描述
我想在 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屋!