Lisp Macros
Lisp宏
So far we've looked at metaprogramming in terms of a simple templating engine
similar to JSP. We've done code generation using simple string
manipulations. This is generally how most code generation tools go about doing
this task. But we can do much better. To get on the right track, let's start off
with a question. How would we write a tool that automatically generates Ant
build scripts by looking at source files in the directory structure?
我们已经看到, 元编程在一个类似jsp的模板引擎方面的应用。我们通过简单的字符串处
理来生成代码。但是我们可以做的更好。我们先提一个问题, 怎样写一个工具, 通过查找
目录结构中的源文件来自动生成Ant脚本。
We could take the easy way out and generate Ant XML by manipulating strings. Of
course a much more abstract, expressive and extensible way is to work with XML
processing libraries to generate XML nodes directly in memory. The nodes can
then be serialized to strings automatically. Furthermore, our tool would be able
to analyze and transform existing Ant build scripts by loading them and dealing
with the XML nodes directly. We would abstract ourselves from strings and deal
with higher level concepts which let us get the job done faster and easier.
用字符串处理的方式生成Ant脚本是一种简单的方式。当然, 还有一种更加抽象, 表达能
力更强, 扩展性更好的方式, 就是利用XML库在内存中直接生成XML节点, 这样的话内存中
的节点就可以自动序列化成为字符串。不仅如此, 我们的工具还可以分析这些节点, 对已
有的XML文件做变换。通过直接处理XML节点。我们可以超越字符串处理, 使用更高层次的
概念, 因此我们的工作就会做的更快更好。
Of course we could write Ant tasks that allow dealing with XML transformations
and write our generation tool in Ant itself. Or we could just use Lisp. As we
saw earlier, a list is a built in Lisp data structure and Lisp has a number of
facilities for processing lists quickly and effectively (head and tail being the
simplest ones). Additionally Lisp has no semantic constraints - you can have
your code (and data) have any structure you want.
我们当然可以直接用Ant自身来处理XML变换和制作代码生成工具。或者我们也可以用Lisp
来做这项工作。正像我们以前所知的, 表是Lisp内置的数据结构, Lisp含有大量的工具来
快速有效的操作表(head和tail是最简单的两个)。而且, Lisp没有语义约束, 你可以构造
任何数据结构, 只要你原意。
Metaprogramming in Lisp is done using a construct called a "macro". Let's try to
develop a set of macros that transform data like, say, a to-do list
(surprised?), into a language for dealing with to-do lists.
Lisp通过宏(macro)来做元编程。我们写一组宏来把任务列表(to-do list)转换为专用领
域语言。
Let's recall our to-do list example. The XML looks like this:
回想一下上面to-do list的例子, 其XML的数据格式是这样的:
<todo name = "housework">
<item priority = "high">Clean the hose</item>
<item priority = "medium">Wash the dishes</item>
<item priority = "medium">Buy more soap</item>
</todo>
The corresponding s-expression version looks like this:
相应的s表达式是这样的:
(todo "housework"
(item (priority high) "Clean the house")
(item (priority medium) "Wash the dishes")
(item (priority medium) "Buy more soap"))
Suppose we're writing a to-do manager application. We keep our to-do items
serialized in a set of files and when the program starts up we want to read them
and display them to the user. How would we do this with XML and some other
language (say, Java)? We'd parse our XML files with the to-do lists using some
XML parser, write the code that walks the XML tree and converts it to a Java
data structure (because frankly, processing DOM in Java is a pain in the neck),
and then use this data structure to display the data. Now, how would we do the
same thing in Lisp?
假设我们要写一个任务表的管理程序, 把任务表数据存到一组文件里, 当程序启动时, 从
文件读取这些数据并显示给用户。在别的语言里(比如说Java), 这个任务该怎么做? 我们
会解析XML文件, 从中得出任务表数据, 然后写代码遍历XML树, 再转换为Java的数据结构
(老实讲, 在Java里解析XML真不是件轻松的事情), 最后再把数据展示给用户。现在如果
用Lisp, 该怎么做?
If we were to adopt the same approach we'd parse the files using Lisp libraries
responsible for parsing XML. The XML would then be presented to us as a Lisp
list (an s-expression) and we'd walk the list and present relevant data to the
user. Of course if we used Lisp it would make sense to persist the data as
s-expressions directly as there's no reason to do an XML conversion. We wouldn't
need special parsing libraries since data persisted as a set of s-expressions is
valid Lisp and we could use Lisp compiler to parse it and store it in memory as
a Lisp list. Note that Lisp compiler (much like .NET compiler) is available to a
Lisp program at runtime.
假定要用同样思路的化, 我们大概会用Lisp库来解析XML。XML对我们来说就是一个Lisp
的表(s表达式), 我们可以遍历这个表, 然后把相关数据提交给用户。可是, 既然我们用
Lisp, 就根本没有必要再用XML格式保存数据, 直接用s表达式就好了, 这样就没有必要做
转换了。我们也用不着专门的解析库, Lisp可以直接在内存里处理s表达式。注意, Lisp
编译器和.net编译器一样, 对Lisp程序来说, 在运行时总是随时可用的。
But we can do better. Instead of writing code to walk the s-expression that
stores our data we could write a macro that allows us to treat data as code! How
do macros work? Pretty simple, really. Recall that a Lisp function is called
like this:
但是还有更好的办法。我们甚至不用写表达式来存储数据, 我们可以写宏, 把数据当作代
码来处理。那该怎么做呢? 真的简单。回想一下, Lisp的函数调用格式:
(function-name arg1 arg2 arg3)
Where each argument is a valid Lisp expression that's evaluated and passed to
the function. For example if we replace arg1 above with (+ 4 5), it will be
evaluated and 9 would be passed to the function. A macro works the same way as a
function, except its arguments are not evaluated.
其中每个参数都是s表达式, 求值以后, 传递给函数。如果我们用(+ 4 5)来代替arg1,
那么, 程序会先求出结果, 就是9, 然后把9传递给函数。宏的工作方式和函数类似。主要
的差别是, 宏的参数在代入时不求值。
(macro-name (+ 4 5))
In this case, (+ 4 5) is not evaluated and is passed to the macro as a list. The
macro is then free to do what it likes with it, including evaluating it. The
return value of a macro is a Lisp list that's treated as code. The original
place with the macro is replaced with this code. For example, we could define a
macro plus that takes two arguments and puts in the code that adds them.
这里, (+ 4 5)作为一个表传递给宏, 然后宏就可以任意处理这个表, 当然也可以对它求
值。宏的返回值是一个表, 然后有程序作为代码来执行。宏所占的位置, 就被替换为这个
结果代码。我们可以定义一个宏把数据替换为任意代码, 比方说, 替换为显示数据给用户
的代码。
What does it have to do with metaprogramming and our to-do list problem? Well,
for one, macros are little bits of code that generate code using a list
abstraction. Also, we could create macros named to-do and item that replace our
data with whatever code we like, for instance code that displays the item to the
user.
这和元编程, 以及我们要做的任务表程序有什么关系呢? 实际上, 编译器会替我们工作,
调用相应的宏。我们所要做的, 仅仅是创建一个把数据转换为适当代码的宏。
For example, a macro similar to our triple C macro we showed earlier looks like
this:
例如, 上面曾经将过的C的求三次方的宏, 用Lisp来写是这样子:
(defmacro triple (x)
`(+ ~x ~x ~x))
(译注: 在Common Lisp中, 此处的单引号应当是反单引号, 意思是对表不求值, 但可以对
表中某元素求值, 记号~表示对元素x求值, 这个求值记号在Common Lisp中应当是逗号。
反单引号和单引号的区别是, 单引号标识的表, 其中的元素都不求值。这里作者所用的记
号是自己发明的一种Lisp方言Blaise, 和common lisp略有不同, 事实上, 发明方言是
lisp高手独有的乐趣, 很多狂热分子都热衷这样做。比如Paul Graham就发明了ARC, 许多
记号比传统的Lisp简洁得多, 显得比较现代)
The quote prevents evaluation while the tilde allows it. Now every time triple
is encountered in lisp code:
单引号的用处是禁止对表求值。每次程序中出现triple的时候,
(triple 4)
it is replaced with the following code:
都会被替换成:
(+ 4 4 4)
We can create macros for our to-do list items that will get called by lisp
compiler and will transform the to-do list into code. Now our to-do list will be
treated as code and will be executed. Suppose all we want to do is print it to
standard output for the user to read:
我们可以为任务表程序写一个宏, 把任务数据转换为可执行码, 然后执行。假定我们的输
出是在控制台:
(defmacro item (priority note)
`(block
(print stdout tab "Prority: " ~(head (tail priority)) endl)
(print stdout tab "Note: " ~note endl endl)))
We've just created a very small and limited language for managing to-do lists
embedded in Lisp. Such languages are very specific to a particular problem
domain and are often referred to as domain specific languages or DSLs.
我们创造了一个非常小的有限的语言来管理嵌在Lisp中的任务表。这个语言只用来解决特
定领域的问题, 通常称之为DSLs(特定领域语言, 或专用领域语言)。
Domain Specific Languages
特定领域语言
In this article we've already encountered two domain specific languages: Ant
(specific to dealing with project builds) and our unnamed mini-language for
dealing with to-do lists. The difference is that Ant was written from scratch
using XML, an XML parser, and Java while our language is embedded into Lisp and
is easily created within a couple of minutes.
本文谈到了两个特定领域语言, 一个是Ant, 处理软件构造。一个是没起名字的, 用于处
理任务表。两者的差别在于, Ant是用XML, XML解析器, 以及Java语言合在一起构造出来
的。而我们的迷你语言则完全内嵌在Lisp中, 只消几分钟就做出来了。
We've already discussed the benefits of DSLs, mainly why Ant is using XML, not
Java source code. Lisp lets us create as many DSLs as we need for our
problem. We can create domain specific languages for creating web applications,
writing massively multiplayer games, doing fixed income trading, solving the
protein folding problem, dealing with transactions, etc. We can layer these
languages on top of each other and create a language for writing web-based
trading applications by taking advantage of our web application language and
bond trading language. Every day we'd reap the benefits of this approach, much
like we reap the benefits of Ant.
我们已经说过了DSL的好处, 这也就是Ant用XML而不直接用Java的原因。如果使用Lisp,
我们可以任意创建DSL, 只要我们需要。我们可以创建用于网站程序的DSL, 可以写多用户
游戏, 做固定收益贸易(fixed income trade), 解决蛋白质折叠问题, 处理事务问题, 等
等。我们可以把这些叠放在一起, 造出一个语言, 专门解决基于网络的贸易程序, 既有网
络语言的优势, 又有贸易语言的好处。每天我们都会收获这种方法带给我们的益处, 远远
超过Ant所能给予我们的。
Using DSLs to solve problems results in much more compact, maintainable,
flexible programs. In a way we create them in Java by creating classes that help
us solve the problem. The difference is that Lisp allows us to take this
abstraction to the next level: we're not limited by Java's parser. Think of
writing build scripts in Java itself using some supporting library. Compare it
to using Ant. Now apply this same comparison to every single problem you've ever
worked on and you'll begin to glimpse a small share of the benefits offered by
Lisp.
用DSL解决问题, 做出的程序精简, 易于维护, 富有弹性。在Java里面, 我们可以用类来
处理问题。这两种方法的差别在于, Lisp使我们达到了一个更高层次的抽象, 我们不再受
语言解析器本身的限制, 比较一下用Java库直接写的构造脚本和用Ant写的构造脚本其间
的差别。同样的, 比较一下你以前所做的工作, 你就会明白Lisp带来的好处。
What's next?
接下来
Learning Lisp is an uphill battle. Even though in Computer Science terms Lisp is
an ancient language, few people to date figured out how to teach it well enough
to make it accessible. Despite great efforts by many Lisp advocates, learning
Lisp today is still hard. The good news is that this won't remain the case
forever since the amount of Lisp-related resources is rapidly increasing. Time
is on Lisp's side.
学习Lisp就像战争中争夺山头。尽管在电脑科学领域, Lisp已经算是一门古老的语言, 直
到现在仍然很少有人真的明白该怎样给初学者讲授Lisp。尽管Lisp老手们尽了很大努力,
今天新手学习Lisp仍然是困难重重。好在现在事情正在发生变化, Lisp的资源正在迅速增
加, 随着时间推移, Lisp将会越来越受关注。
Lisp is a way to escape mediocrity and to get ahead of the pack. Learning Lisp
means you can get a better job today, because you can impress any reasonably
intelligent interviewer with fresh insight into most aspects of software
engineering. It also means you're likely to get fired tomorrow because everyone
is tired of you constantly mentioning how much better the company could be doing
if only its software was written in Lisp. Is it worth the effort? Everyone who
has ever learned Lisp says yes. The choice, of course, remains yours.
Lisp使人超越平庸, 走到前沿。学会Lisp意味着你能找到更好的工作, 因为聪明的雇主会
被你与众不同的洞察力所打动。学会Lisp也可能意味着明天你可能会被解雇, 因为你总是
强调, 如果公司所有软件都用Lisp写, 公司将会如何卓越, 而这些话你的同事会听烦的。
Lisp值得努力学习吗? 那些已经学会Lisp的人都说值得, 当然, 这取决于你的判断。
Comments?
你的看法呢?
Whew. That's enough. I've been writing this article, on and off, for months. If
you find it interesting, have any questions, comments, or suggestions, please
drop a note at coffeemug@gmail.com. I'll be glad to hear your feedback.
这篇文章写写停停, 用了几个月才最终完成。如果你觉得有趣, 或者有什么问题, 意见或
建议, 请给我发邮件coffeemug@gmail.com, 我会很高兴收到你的反馈