在周末花费了一些时间,我决定最终探索即将发布的Scala 2.10 宏中的一项新功能。
宏也是用Scala编写的,因此从本质上讲,宏是一段Scala代码,在编译时执行,可操纵和修改Scala程序的AST。
为了做一些有用的事情,我想实现一个简单的调试宏。 我想我不是一个人使用println-debugging,而是通过插入如下语句来进行调试:
println('After register; user = ' + user + ', userCount = ' + userCount)
运行测试,并检查输出是什么。 在变量之前写变量名很麻烦,所以我想写一个宏来帮我。 那是:
debug('After register', user, userCount)
应该与第一个代码段具有相同的效果(它应该生成与上面的代码相似的代码)。
让我们逐步了解如何实现这样的宏。 我使用的scala宏页面上有一个很好的入门指南。 以下解释的所有代码都可以在GitHub的scala-macro-debug项目中找到 。
1.项目设置
为了舒适地进行实验,我们需要首先设置一个简单的项目。 我们将至少需要两个子项目:一个用于宏,另一个用于测试宏。 那是因为宏必须和之前分别编译以及使用它们的代码(因为它们会影响编译过程)。
而且,宏子项目需要依赖于scala-compiler,才能访问反射和AST类。
一个简单的SBT构建文件可能如下所示: Build.scala 。
2.你好世界!
“你好,世界!” 始终是一个很好的起点。 因此,我的第一步是编写一个宏,该宏在编译时将hello()
扩展为println('Hello World!')
。
在宏子项目中,我们必须创建一个新对象,该对象定义hello()
和宏:
package com.softwaremill.debug
import language.experimental.macros
import reflect.macros.Context
object DebugMacros {
def hello(): Unit = macro hello_impl
def hello_impl(c: Context)(): c.Expr[Unit] = {
// TODO
}
}
这里有几件重要的事情:
- 我们必须导入
language.experimental.macros
,以在给定的源文件中启用宏功能。 否则,我们会得到编译错误,提醒我们有关导入的信息。 -
hello()
的定义使用macro
关键字,后跟实现该宏的方法 - 宏实现有两个参数列表:第一个是上下文(您可以将其视为编译上下文),第二个是我们方法的参数列表的镜像–这里为空。 最后,返回类型也必须匹配-但是在方法中,我们有一个返回类型单元,在宏中,我们返回了一个类型为unit的表达式(包装了AST的一部分)。
现在到实现,这很短:
def hello_impl(c: Context)(): c.Expr[Unit] = {
import c.universe._
reify { println('Hello World!') }
}
逐行:
- 首先,我们导入“ universe”,从而可以方便地访问AST类。 请注意,返回类型为
c.Expr
–因此它是与路径相关的类型,取自上下文。 您会在每个宏中看到该导入。 - 因为我们要生成打印“ Hello World!”的代码,所以需要为其创建一个AST。 Scala提供了一种
reify
方法(reify也是一个宏-编译宏时使用的宏),而不是手动构造它(可能,但是看起来不太好),它将给定的代码转换为Expr[T]
(表达式包含AST及其类型)。 由于println
具有类型为unit的类型,所以经过简化的表达式的类型为Expr[Unit]
,我们可以将其返回。
用法很简单。 在测试子项目中,编写以下内容:
object DebugExample extends App {
import DebugMacros._
hello()
}
并运行代码(例如,使用SBT Shell中的run
命令)。
3.打印出一个参数
打印Hello World很好,但是打印参数更好。 第二个宏就是这样做的:它将把printparam(anything)
转换成println(anything)
。 它不是非常有用,并且与我们所看到的非常相似,但有两个关键区别:
def printparam(param: Any): Unit = macro printparam_impl
def printparam_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
import c.universe._
reify { println(param.splice) }
}
第一个区别是该方法接受参数param: Any
。 在宏实现中,我们必须进行镜像–但是与返回类型相同,而不是Any
,我们接受Expr[Any]
,就像在编译时对AST进行操作一样。
第二个区别是splice
的用法。 这是Expr
一种特殊方法,该方法只能在reify
调用内使用,并且与reify有点相反:它将给定的表达式嵌入正在进行正则化的代码中。 在这里,我们有一个param
,它是一个Expr
(即树+类型),并且我们希望将该树作为println
的子println
; 我们希望将由param
表示的值传递给println
,而不是AST。 在Expr[T]
上调用的splice
返回T
,因此经过验证的代码进行类型检查。
4.单变量调试
现在让我们进入调试方法。 首先也许让我们实现一个单变量调试,即将debug(x)
转换为诸如println('x = ' + x)
。
这是宏:
def debug(param: Any): Unit = macro debug_impl
def debug_impl(c: Context)(param: c.Expr[Any]): c.Expr[Unit] = {
import c.universe._
val paramRep = show(param.tree)
val paramRepTree = Literal(Constant(paramRep))
val paramRepExpr = c.Expr[String](paramRepTree)
reify { println(paramRepExpr.splice + ' = ' + param.splice) }
}
当然,新事物是生成前缀。 为此,我们首先将参数的树转换为String
。 内置方法show
正是这样做的。 这里有一点注意; 当我们将AST转换为String
,输出看起来可能与原始代码有些不同。 对于在方法内部声明的val,它将仅返回val名称。 对于类字段,您将看到类似DebugExample.this.myField
。 对于表达式,例如left + right
,您将看到left.+(right)
。 我认为这并不完美,但足够可读。
其次,我们需要创建一棵树(这次是手工)来表示一个常量String
。 在这里,您只需要知道要构造什么,例如,检查通过验证创建的树(或阅读Scala编译器的源代码)。
最后,我们将该简单树转换为String
类型的表达式,并将其拼接到println
。 例如运行这样的代码:
object DebugExample extends App {
import DebugMacros._
val y = 10
def test() {
val p = 11
debug1(p)
debug1(p + y)
}
test()
}
输出:
p = 11
p.+(DebugExample.this.y) = 21
5.最终产品
如上所述,实现完整的调试宏仅引入了一个新概念。 完整的源代码有点长,因此您可以在GitHub上查看它 。
在宏实现中,我们首先为每个参数生成一棵树(AST)–它表示打印常量或表达式。 然后,我们将树与分隔符( ', '
)交错', '
以便于阅读。
最后,我们必须将树列表转换为表达式。 为此,我们创建一个Block
。 一个块包含一个应执行的语句列表以及一个作为整个块结果的表达式。 在我们的例子中,结果当然是()
。
现在我们可以愉快地调试了! 例如,编写:
debug('After register', user, userCount)
执行时将打印:
AfterRegister, user = User(x, y), userCount = 1029
加起来
那篇文章很长,很高兴有人做到了。 无论如何,宏看起来真的很有趣,开始自己编写宏非常简单。 您可以在GitHub上找到一个简单的SBT项目以及此处讨论的代码( scala-macro-debug项目 )。 而且我想很快我们将看到大量的宏观杠杆项目。 已经有一些,例如Expectation或Macrocosm 。
参考: 从Scala宏开始: Adam Warski博客的Blog中来自我们JCG合作伙伴 Adam Warski 的简短教程 。
翻译自: https://www.javacodegeeks.com/2012/12/starting-with-scala-macros-a-short-tutorial.html