宏(MACROS)到目前为止所看到的特性都和我们在比较新的动态语言中发现的相类似,例如Ruby,它也允许你使用匿名块处理对象收集,正如我们在前面用lambda和map函数所做的一样。所以,现在让我们来转变一下方向,看看独属于Lisp的特性:宏(macros)。
Scheme和Common Lisp都有宏系统。人们在提到Lisp时总说它是“可编程的程序设计语言”,其实指得就是这个。有了宏,你实际上就可以和编译器建立关联,重新定义语言本身。此时Lisp统一的语法才真正开始挥效用,所有的事情都变得有趣起来。
举个简单的例子,我们可以看一下循环。在Scheme语言中,最初并没有定义循环,典型的对某集合进行迭代的方式是使用map或者递归函数调用。多亏有一个编译器小窍门——尾调用优化递归(tail-call optimizations recursion)——可以采用而不必担心会挤爆栈。下面将介绍一个非常灵活的do命令并应用它来执行一个循环,实现的程序如下:

Code
(do ((i 0 (+ i 1)))
((= i 5) #t)
(display "Print this "))
上面程序中定义了一个索引变量i,初始化为0,设置按照增量1迭代增长。当表达式(= i 5)的值为真时,循环中止,返回#t(它和Java中的布尔值true相当)。在循环里我们只是打印了一个字符串。
如果我们所需要做的只是一个简单的循环,上面这个例子就有很多冗余的公式化代码了。在很多情况下更可取的应当是简单直接的实现方式:

Code
(dotimes 5 (display "Print this"))
多亏了宏(macros),才有可能适当地使用称为define-syntax函数,把关于dotimes的特殊语法添加进语言:

Code
(define-syntax dotimes
(syntax-rules ()
((dotimes count command) ; Pattern to match
(do ((i 0 (+ i 1))) ; What it expands to
((= i count) #t)
command))))
执行上述命令可以告诉系统,任何对dotimes的调用都要被特别对待。Scheme将用我们定义的语法规则匹配一个模式,并在将结果送到编译器之前将其展开。在这个例子中,模式是(dotimes count command),它被转换为标准的do循环。
在REPL中执行该语句,你会得到如下结果:

Code
#|kawa:14|# (dotimes 5 (display "Print this "))
Print this Print this Print this Print this Print this #t
上述例子之后必然产生两个问题。第一,为什么我们需要使用宏(macro)?用一个常规的函数不能做这些事情么?答案是“不可以”。任何对函数的调用实际上在开始之前都会触发对它所有参数的求值操作,在上面的例子中就不会发生这种情况。比方说,你怎样处理(do-times 0 (format #t "Never print this"))呢?当求值需要被延迟时,只有宏(macro)才能完成这个功能。
其次,我们在宏里用了变量i,如果在command表达式中碰巧有一个变量取相同的名字,这会不会产生冲突呢?这点不必担心,Scheme的宏以“卫生”著称。编译器会自动检测并熟知如何处理这样的命名冲突,对程序员是完全透明的。
了解到这些情况后,试想一下在Java中添加你自己的循环结构,这近乎不可能。也可以说,不是非常可能,毕竟编译器是开源的,所以你可以自由下载并恰当使用,但这真的是一个不太现实的选择。在其它动态语言中,闭包可以给你多些自由,对语言按照自己的习惯做些改动,但是仍然存在这种情况:他们的结构并没有足够灵活和强大到可以让你自由调整语法的程度。
这种能力就是为什么每当元编程语言或特定领域语言被提及时,Lisp总是以胜利者姿态出现的原因。Lisp程序员长期以来一直是彻头彻尾的“自底向上编程(bottom-up programming)”的冠军,因为当语言本身已经被调节为适合你的问题领域时,障碍会少许多。
在Java中调用Scheme代码将别的语言运行在JVM之上的一个主要好处是,不管代码用何种语言写成,都可与现存的应用进行整合。因此很容易想象,可以用Scheme来模型化一些复杂的具有易变趋势的业务逻辑,然后将它嵌入一个比较稳定的Java框架中。规则引擎Jess(
www.jessrules.com)是一个很好的范例,它运行于JVM之上,但是用自己的类Lisp语言来声明规则。
但是让不同的程序设计语言以一种界限清晰的方式协同工作还是一个棘手的问题,尤其是像Java和Lisp这样存在天壤之别的语言。如何做这种整合并没有标准,所有活跃在JVM上的方言都以不同的方式处理着问题。Kawa对于Java整合的支持相对较好,所以在下面的例子中,我们将继续用它来研究怎样用 Scheme代码来定义一个Swing GUI。
在Java程序中运行Kawa代码是很简单的:

Code
import java.io.InputStream;
import java.io.InputStreamReader;
import kawa.standard.Scheme;
public class SwingExample {
public void run() {
InputStream is = getClass().getResourceAsStream("/swing-app.scm");
InputStreamReader isr = new InputStreamReader(is);
Scheme scm = new Scheme();
try {
scm.eval(isr);
} catch (Throwable schemeException) {
schemeException.printStackTrace();
}
}
public static void main(String arg[]) {
new SwingExample().run();
}
}
在这个例子中,首先会在类路径上寻找包含Scheme程序的叫做swing-app.scm的文件,然后创建解释程序kawa.standard.Scheme的实例,调用它来解释文件中内容。
Kawa还不支持在Java 1.6中引入的JSR-223规定的脚本APIs(javax.scripting.ScriptEngine等),如果你需要能做这种事情的Lisp,最好的选择应该是SISC。
在Scheme中调用Java库在我们开始写大型Lisp程序之前,是时候找个比较合适的编辑器了,否则光是验证括号匹配的工作就够让人发疯了。最受欢迎的选择之一肯定是Emacs,毕竟它可用自己的Lisp方言进行编程,不过对于Java开发者继续使用Eclipse可能更舒服些。如果你是这种情况就需要在工作开始之前先安装一个免费的SchemeScript插件。你可以在
这个网站找到它。这里还有一个称为
Cusp的插件,可以用于Common Lisp的开发。
现在,我们可以来看一下swing-app.scm的具体内容,以及用Kawa定义一个简单的GUI都需要做什么样的工作。这个例子将会打开一个带有按钮(button)的frame,按钮点击一次后它就会被禁用。

Code
(define-namespace JFrame )
(define-namespace JButton )
(define-namespace ActionListener )
(define-namespace ActionEvent )
(define frame (make JFrame))
(define button (make JButton "Click only once"))
(define action-listener
(object (ActionListener)
((action-performed e :: ActionEvent) ::
(*:set-enabled button #f))))
(*:set-default-close-operation frame (JFrame:.EXIT_ON_CLOSE))
(*:add-action-listener button action-listener)
(*:add frame button)
(*:pack frame)
(*:set-visible frame #t)
最初几行用define-namespace命令为将要用到的Java类定义缩略名,这同Java的import声明功能类似。
然后定义了frame和button,利用make函数可以创建Java对象。创建button时,我们提供一个字符串作为参数传给构造函数,Kawa可以很智能的将它翻译成需要的java.lang.String对象。
现在让我们跳过ActionListener的定义,先来看一下最后5行代码。这里的符号*:用于触发对象中的方法。例如,(*:add frame button)的功能就等同于frame.add(button)。你要注意到Scheme特有的,可以自动将方法名从Java中的骆驼拼写风格转换为小写的以连字符分隔单词。例如,set-default-close-operation将被转换为setDefaultCloseOperation。这里另外一个细节是:.可被用来访问静态域,(JFrame:.EXIT_ON_CLOSE)等同于JFrame.EXIT_ON_CLOSE。
现在来回头看一下ActionListener。这里用object函数创建了一个实现了java.awt.event.ActionListener接口的匿名类,action-performed函数被用来调用button上的setEnabled(false)方法。此时还需要添加些信息可以让编译器知道action-performed是ActionListener接口中定义的void actionPerformed(ActionEvent e)的实现。早先我们曾经说过,正常情况下在Scheme中并不需要类型,但是此时,当与Java协同工作时,编译器就需要多知道一些信息。
当你有了这两个文件后,编译SwingExample.java,并且确认将编译后的类和swing-app.scm文件放到类路径上,接下来就可运行java SwingExample来看看GUI的效果。你同样也可以用load函数: (load "swing-app.scm")在REPL中执行文件中的代码,这开启了动态操纵GUI构件的先河。例如,你可以通过在REPL中执行(*:set-text button "New text")来快速更改button上的文字,而且可以立即看到修改结果生效。
当然,这个例子只是想简单的演示如何从Kawa中调用Java,无论如何它都不是你能想象中的最优质的Scheme代码。如果你确实想要在Scheme中定义一个大型Swing UI,那你最好提升一点抽象级别,用一些精选的函数和宏来隐藏凌乱的整合代码。
资源真心希望我的文章能引起你对Lisp的些许兴趣。请相信我,还有大量有待探索的东西。如果想了解更多内容可以查看下列资源:
关于作者Per Jacobsson是位于洛杉矶的eHarmony.com的软件架构师,应用Java已有10年历史,近两年成为Lisp的狂热爱好者。你可以通过
pjacobsson.com与他取得联系。