1.1 宏

Lisp 的核心在编程领域中的某些方面具有一定的优势。 ——lisp 创始人谦逊之词

本书是介绍 lisp 的宏编程。与大部分编程书不同的是,本书不是只给个大概的介绍,而是
通过一系列的教程和实例,让你尽可能快速高效地掌握复杂的宏编程。掌握宏是从中级 lisp
程序员变成专业 lisp 程序员的最后一步。

宏是 lisp 编程语言的最大优势,同时也是所有编程语言的最大优势。通过宏,你可以实
现其他语言不能无法实现的功能。这是因为宏可以将 lisp 编程其他的编程语言,然后又
变回来,有经验的宏程序员会发现其他的语言都只是 lisp 的皮毛。这点很重要。lisp 的
特别之处在于它实际上是在一个更高级别编程。大多数语言都创建和制定句法和语义规
则,而 lisp 就比较通用,且可塑性强。在 lisp 中,你可以制定规则。

lisp 比其他的编程语言具有更深厚的历史。在该领域中,一些顶尖的计算机科学家为之
奋斗,让 lisp 成为了最强大和通用的编程语言。lisp 也有简明的数字标准、多个出色的
开源实现以及比其他语言更方便的宏。本文只使用 COMMON LISP,但大部分思路都适
用于其他的 lisp 方言,如 Scheme。也就是说,如果想要编写宏,希望本书能说服你
COMMON LISP 才是最好的 lisp 方言。正如其他的 lisp 方言各有其优势,COMMON
LISP 是宏专业人员当之无愧的首选。

在正确地设计编程语言方面,COMMON LISP 的设计者做的很棒。特别是在考虑到实现
的质量上,毫无疑问,COMMON LISP 是现代程序员迄今为止最好的编译环境。作为一
个程序员,你永远可以指望 COMMON LISP 按照其该有的方式运行。即便是设计者和实
现的解释器都做的很对,有些人还是觉得他们没有向我们描述清楚对的原因。对大部分
外行人来说,COMMON LISP 看起来就像个有一大堆奇怪特性的集合,因此被拒之门外,
他们转而使用一种更容易满足的语言,这注定无法体验到宏的强大之处。虽然这不是本
书的主要目的,但本书可以作为 COMMON Lisp 这门神奇语言中许多优秀特性的导览。
大部分语言都设计的易于实现;而 COMMON LISP 则设计成编程时很强大。我真诚地
希望COMMON LISP的创造者能够欣赏这本书,因为它是对该语言高级宏功能的最完整
和最容易理解的处理方法之一,同时本书也是宏这一主题的海洋中愉快的一滴水。

宏在1963年由 Timothy Hart 发明,其历史几乎与 lisp 本身一样,并非偶然。然而,大多
数lisp程序员仍然没有最大限度地使用宏,而且其他程序员也根本没有使用宏。这一直是
高级 lisper 的难题。既然宏是这么好,为什么没有人都一直用它们呢?虽然最聪明、最坚
定的程序员确实总是以 lisp 宏为终点,但很少有人从宏开始他们的编程生涯。要理解宏为
什么这么好,就需要理解 lisp 有什么是其他语言没有的。这需要对一些不怎么强大的语言
有所了解。可悲的是,大多数程序员在掌握了一些语言后,就失去了学习的意愿,也从来
没有去了解什么是宏,或者如何利用宏。但任何语言的顶尖程序员总是被迫学习某种编程
的方法:宏。聪明、有决心且好奇的程序员最终都会选择 lisp,因为它是编写宏的最佳语言。

虽然优秀的程序员必然是少数,但随着整个编程人口的增长,优秀的程序员的数量也在增
加。编程界很少有程序员意识到宏的强大,了解的也很少,但这种情况正在改变。因为宏
能让生产力的倍增,无论世界是否准备好,宏的时代正在到来。 本书旨在为不可避免的未
来做一个基础准备:一个宏的世界。请做好准备。

属于宏的传统智慧是,只有在必要时才使用它们,因为它们可能难以理解,同时包含极其
微妙的错误。而且如果你把所有东西都看成是函数,这可能会以某些诧异的方式限制你。
这些并不是 lisp 宏系统本身的缺陷,而是一般宏编程的特征。和所有的技术一样,工具越
强大,滥用它的方法就越多。而且,就编程结构而言,lisp 的宏是最强大的工具。

一个有趣的类比就是,学习 lisp 的宏和学习 C 语言的指针一样。大多数 C 语言初学者都能
迅速掌握该语言的大部分。函数、类型、变量、算术表达式:从小学数学到简单的编程体验,
所有这些都与初学者以前可能有过的学习经历相似。但大多数C 程序员新手在学习指针时都
会碰壁。

指针是经验丰富的C程序员的第二天性,他们中的大多数人认为,要正确使用C语言,必须完
全理解指针。因为指针是如此的基础,在格式或学习目的上,大部分老练的C程序员不会在使
用方面有限制。尽管如此,许多C语言新手觉得指针是种不必要的复杂化,并避免使用它们,
从而出现在 FORTRAN 或其他语言中的症状,即有语言的重要特性被忽视。这种病症是对语
言特性的无知,而不是糟糕的编程风格。一旦充分理解了这些特性,正确的风格就会显而易见。
本书的一个次要主题,适用于任何编程语言,就是在编程中,编程风格不是直接追求的东西。
只有在理解缺失的情况下,风格才是必要的!

与C语言的指针一样,宏是lisp的一个特征,但人们对它的理解往往不深,如何正确使用宏的
智慧非常分散和理想化。如果在考虑宏的时候,你发现自己依赖于风格化的谚语,比如:

宏改变了lisp代码的语法。
宏作用于程序的解析树上。
只有在函数无法完成的情况下才使用宏。

当涉及到宏编程时,你可能没有一个整体的概念。而这正是本书希望解决的问题。

有关宏的构建,好的参考资料或教程比较少。Paul Graham的《On Lisp》是一个例外。对宏感
兴趣的人来说,《On Lisp》中的每一个字都是必读的。《On Lisp》和 Graham 的其他著作都
是你现在正在阅读的这本书的创作的重要灵感来源。正是因为 Paul Graham 和其他 lisp 作家的
努力,宏为程序员提供的力量被广泛讨论。然而不幸的是,它仍然被广泛误解。尽管从
《On Lisp》中可以得到有关宏编程的智慧,但很少有程序员将宏与他们现实生活中的编程问题
联系起来。就像《On Lisp》会告诉你不同类型的宏一样,本书会教你如何使用它们。

宏的编写是个反思和迭代的过程。复杂的宏都来自于较简单的宏,通常要经过一系列漫长的改进-
测试周期。更重要的是,识别在何处应用宏是种后天的技能,可通过编写宏来训练。当你写程序
时,作为一个有意识的人,无论你是否意识到,都在遵循某个系统和过程。程序员都有关于编程
工具如何工作的概念模型,而代码的创建则是这个概念模型的直接逻辑结果。一旦聪明的程序员
开始把编程行为视为一个逻辑程序,合乎逻辑的下一步就是让这个过程从自动化本身中受益。
毕竟,程序员接受的培训正是为了实现这一目标:使程序自动化。

理解宏的关键的第一步是认识到,如果没有仔细的计划和大量的努力,任何程序都会有多余的模式
和不灵活的抽象。这几乎在所有的大型软件项目中都可以看到,可能是重复的代码,或者是由于作
者没有正确的抽象概念而导致的不必要的复杂代码。有效使用宏需要识别这些模式和抽象,然后创
建代码来帮助你。仅仅了解如何编写宏是不够的;专业的lisp 程序员需要知道为什么要编写宏。

刚接触 lisp 的 C 程序员经常会犯这样的错误:认为宏的主要目的是提高代码在运行时的效率。虽然
宏在这个任务上往往非常有用,但到目前为止,宏最常见的用途是使编写所需应用程序的工作更容
易。由于大多数程序中的大部分模式都是重复复制的,且其抽象的通用性没有得到充分的利用,
因此适当设计的宏可以使编程在字面上有新的表达平面。其他语言是僵化和具体的,而 lisp 是流动
和通用的。

本书不是介绍 lisp 的。本书的主题和材料是针对那些对宏的作用感到好奇的非lisp语言的专业程序员,
以及那些准备真正学习lisp特别之处的中级lisp学员。本书假定有 lisp 编程的基础和中级知识,但对
闭包和宏的理解并没有太大的要求。

本书也不是关于理论的。所有的例子都涉及可运行的代码,这可以帮助改善你今后的编程。本书是
利用高级编程技术来帮助你更好地编程。与其他许多刻意使用简单编程风格以提高可读性的编程书籍
相比,本书认为教授编程的最佳方法是充分利用语言。尽管提供的许多代码示例使用了COMMON LISP
复杂的功能,但这种潜在的不熟悉的功能在使用时被描述。对于自检,如果你已经阅读并理解了
第2章:闭包第3章:宏基础 中的所有内容,就本书而言,你可以认为自己已经过了理解
lisp的中间阶段。

学习 lisp 的部分是需要靠你自己去发掘,且本书不会剥夺你的这个权利。注意,本书的进度比大多数
书要快,比你之前的要快。要理解本书中的一些代码,你可能需要查阅其他COMMON LISP教程或参考
资料。在介绍完基础知识后,我们将直接进入解释一些迄今为止最先进的宏研究,其中大部分内容都是
在一个巨大的、未被开发的灰色区域的知识领域中。就像所有的高级宏编程一样,本书在很大程度上关
注宏的组合。这个话题有个可怕的名声,很少有程序员能很好地理解它。宏的组合代表了当今编程语言
中最广阔、最肥沃的研究领域。学术界已经从类型、对象和 prolog式逻辑中得出了大部分
有趣的结果,但宏编程仍然是个巨大的、有缺口的黑洞。没有人真正知道后面是什么。我们所知道的是,
是的,它很复杂,很可怕,目前看来潜力无穷。与其他太多的编程理念不同,宏既不是用来发表无用
理论文章的学术概念,也不是空洞的企业软件流行语。宏是黑客的最好朋友。宏让你的编程更聪明,
而不是更难。大多数程序员在了解了宏之后,都不想在没有宏的情况下进行编程。

prolog 是一种逻辑编程语言。它创建在逻辑学的理论基础之上, 诞生与 1972 年,最初被运用于
自然语言等研究领域,具体介绍请参考:https://en.wikipedia.org/wiki/Prolog

虽然大多数lisp书籍都是为了让lisp更受欢迎而写的,但我完全不关心lisp的日常公众吸引力。lisp并没有
消失。如果我可以在余下的编程生涯中继续使用lisp作为秘密武器,我将会非常高兴。如果这本书只有
一个目的,那就是激发人们对宏的学习和研究,就像我在《On Lisp》中受到的启发一样。我希望本书的
读者也能受到这样的启发,以至于有一天我可能会享受到更好的lisp宏工具和更有趣的lisp宏书籍。

仍然对lisp的力量感到敬畏。
你们谦卑的作者。
Doug Hoyte [doug@hoytech.com](mailto:doug@hoytech.com)

1.2 U 语言

由于讨论宏涉及到讨论本身,所以需要明确本书的书写习惯。。正如你正在阅读和理解
的东西所传达给你的那样,我现在所写的本身就是个值得规范化和分析的表达系统。

没有人比 Haskell Curry(Foundations Of Mathematical Logic 的作者 )更了解这一点。
这是因为 Curry 不仅想将思想规范化,而且还想表示思想本身。他认为把作者与读者交
流的语言中的概念抽象是有必要的,并把这种语言称为 U 语言。

每一项调查都必须通过语言的方式在人与人之间交流。在我们研究之初,希望大家关注这个
明显的事实,可以给正在使用的语言起个名字,并明确说明它的几个特点。我们将把正在
使用的语言称为 “U语言”。如果不是因为语言与我们的工作比其他大多数人的工作更密切
相关,那么呼吁关注它就没有意义。

在本书中,将使用 斜体 来表示一些关键的概念和要点。用 粗体 来表示程序中的特殊结构、
函数、宏和其他的标识符,不论它们有没有出现过。注意有些词有多种含义,例如 lambda
是COMMON LISP的宏,而 lambda 是概念;let 是特殊结构,而 list 则是个 let 结构。

  1. defun example-program-listing()
  2. ‘(this is
  3. (a (program
  4. (listing)))))

在本书中,新出现的程序代码都会单独的显示代码框中。正如 example-program-listing
函数的定义一样,代码是为重复使用而设计,或者为恰当地实现例子而设计的。但有时我们仅希
望展示一点代码的使用,或者只是想讨论一些表达式的属性,并不想与书面文本脱离太多。在这
些情况下,代码或代码的使用示例将像这样出现:

  1. (this is
  2. (demonstration code))

许多教学编程的文章都使用大量孤立的、设计好的例子来说明问题,但却忘了将其与现实相结合。
本书试图用尽量少而直接的例子来说明宏观的编程思想。有些文章试图在例子中使用可爱、古怪
的标识符名称或肤浅的类比来掩盖其无聊。但我们的例子只是为了说明观点。开个玩笑,这本书
首先试图不把自己(或任何东西)看得太严肃。但与阅读其他书籍不同的是,你需要去寻找它。
由于 lisp 的交互性质,计算一个简单表达式的结果往往比等量的 U 语言表达地要多。在这种情
况下,我们将这样显示 COMMON LISP Read Evaluate Print Loop(称为REPL)的输出:

  1. * (this is
  2. (the expression
  3. (to evaluate)))
  4. THIS-IS-THE-RESULT

注意输入的文本是小写的,但 lisp 返回的文本是大写的。能简便地区分 REPL 的输入输出是
COMMON LISP 的一个特点。更确切地说,这个特点能使我们立即知道 LISP 文件是否已被
lisp 阅读器处理。星号()代表一个提示。星号()是一个理想的符号,因为它不会与输
入字符相混淆,并且它的高像素数使它在REPL输出时更加突出。

编写复杂的 lisp 宏是一个迭代的过程。没有人会用其在他语言程序中常见的轻率风格,写出一
个长达几页的宏。一部分原因是 lisp 代码每页包含的信息比大多数其他语言多得多。另部分原
因是 lisp 鼓励程序员发展他们的程序:根据应用的需要,通过一系列的改进来完善它们。

本书区分了 lisp 的类型,如 COMMON LISP,Scheme 和 building material(一种更抽象的 lisp 概念)。
还介绍了 lisp 编程语言和非 lisp 编程语言之间的区别。当需要谈论非 lisp 语言时,会避免直接指
明语言名字以减少树敌。为了做到这一点,我们采用了下面这个不寻常的定义。

没有 lisp 宏的语言就是 Blub。

U 语言中的 Blub 一词来自 Paul Graham 的一篇文章《Beating the Averages》,Blub 是一种隐喻,
用来强调 lisp 与其他语言不同的事实。Blub的特征有中缀语法、恼人的类型系统和残缺的对象系统,
但不同 blub 的唯一统一的特征是没有 lisp 宏。Blub 术语很有用,因为有时理解一个高级宏的最简
单方法就是考虑为什么这个技术在Blub 中不可能实现。Blub 术语的目的不是为了取笑非 lisp 语言。

为了说明写宏的迭代过程,本书采用了这样的惯例:在定义不完整或尚未以其他方式改进的函数
和宏的名称后面加上百分数(%)字符。在确定最终版本之前,多次修订可能会导致一个名称的末
尾出现多个 % 字符。

  1. (defun example-function% () ;first try
  2. t)
  3. (defun example-function%% () ; second try
  4. t)
  5. (defun example-function () : got it!
  6. t)

Curry 将宏描述为元编程(metaprogramming)。元程序的唯一目的是使程序员能够更好地编写软件。
尽管所有的编程语言都在不同程度上采用了元编程,但没有一种语言像 lisp 那样彻底地采用了它。
没有其他任何一种语言要求程序员以方便元编程技术的目的写代码。这就是为什么 lisp 程序在非
lisp 程序员看来很奇怪:lisp 代码的表达方式是其元编程需求的直接结果。正如本书试图描述的
那样,lisp 的在 lisp 本身中编写元程序的设计使得 lisp 具有惊人的生产力优势。然而,由于我们
在 lisp 中创建元程序,我们必须牢记元编程与 U 语言规范不同。我们可以从不同的角度讨论不同
的元语言,但只有一种 U 语言。库里为他的 U 语言明确了这一点:

我们可以形成具有任何数量级别的语言层次结构。然而,无论有多少个层次,U语言都将是
最高的层次:如果有两个层次,它将是元语言;如果有三个层次,它将是元-元语言;以此
类推。因此,U语言和元语言这两个术语必须保持区别。

当然,这是本关于 lisp 的书,而 lisp 的逻辑系统与库里所描述的非常不同,所以我们将采用很少他
作品中的其他惯例。但库里对逻辑和元编程的贡献至今仍激励着我们。这不仅是因为他对符号引文有深刻
的见解,而且还因为他的U语言措辞优美,执行高效。

1.3 Lisp 实用程序

《On Lisp》是本你要么理解,要么不理解的书。你要么崇拜它,要么害怕它。从它的书名
开始,《On Lisp》是关于创建编程抽象的,这些抽象是 Lisp 之上的层次。在创建了这些
抽象之后,就可以自由地创建更多的编程抽象,这些抽象是早期抽象的连续层次。

在几乎所有值得使用的语言中,语言的大部分功能都是用语言本身实现的;Blub 语言通常
有大量用 Blub 编写的标准库。当连程序员都不想用目标语言编程时,你可能也不会想这
样做。

但即使考虑了其他语言的标准库,lisp 也是不同的。从其他语言是由原语( primitive )组成
的意义上讲,lisp 是由元原语(meta-primitive)组成的。一旦宏如 COMMON LISP 那样被
标准化,语言的其他部分就可以从根本上被引导发展起来了。大多数语言只是试图提供一套
足够灵活的这些原语,而 lisp 提供了一个允许任何和所有种类的原语的元编程系统。另一种
思考方式是,lisp完全摒弃了原语的概念。在 lisp 中,元编程系统并没有停止在任何所谓的
原语上。这些用于构建语言的宏编程技术有可能,事实上也是人们所希望的,它可以一直
延续到用户应用程序中。即使是由最高级别的用户编写的应用程序,也是 lisp 洋葱上的宏层,
通过迭代而不断增长。

从这个角度来看,语言中存在原语是一个问题。只要有原语,系统的设计就会有障碍和非正
交性。当然,有时这是有道理的。大多数程序员都能把单个机器码指令当作原语,让他们的
C 或 lisp 编译器来处理。但是lisp用户要求对其他几乎所有的东西进行控制。就给予程序员
的控制权而言,没有其他语言能像lisp那样完整。

听从《On Lisp》的建议,本书是作为洋葱上的另一层设计的。就像程序在其他程序上分层
一样,本书也是《On Lisp》的更深一层。这本书的中心主题是当设计良好的实用程序结合
在一起时,可以发挥出大于各部分之和的生产力优势。本节介绍了一系列来自《On Lisp》
和其他资料的实用工具。

  1. (defun mkstr (&rest args)
  2. (with-output-to-string (s)
  3. (dolist (a args) (prince a s))))
  4. (defun symb (krest args)
  5. (values (intern (apply #'mkstr args))))

symb 是创建符号的通用方法,分层在 mkstr 之上。由于符号可以被任何任意的字符串引用,
而且以编程方式创建符号是非常有用的,因此 symb 是宏编程的一个基本工具,在本书中被
大量使用。

  1. (defun group (source n)
  2. (if (zerop n) (error "zero length"))
  3. (labels ((rec (source acc)
  4. (let ((rest (nthcdr n source)))
  5. (if (consp rest)
  6. (rec rest (cons
  7. (subseq source 0 n)
  8. acc))
  9. (nreverse
  10. (cons source acc))))))
  11. (if source (rec source nil) nil)))

group 是另一个在编写宏时经常出现的工具。原因一是需要镜像运算符,如 COMMON LISP
setfpsetf,它们已经对参数进行了分组。原因二是分组通常是结构化相关数据的
最佳方式。由于我们经常使用这种功能,所以尽可能地使之抽象化是有意义的。Graham 的
分组将按由参数 n 指定的分组量进行分组。在 setf 这样的情况下,参数被分组成对,
n 是 2。

  1. (defun flatten (x)
  2. (labels ((rec (x acc)
  3. (cond ((null x) acc)
  4. ((atom x) (cons x acc))
  5. (t (rec
  6. (car x)
  7. (rec (cdr x) acc))))))
  8. (rec x nil)))

flatten 是《On Lisp》中最重要的实用工具之一。给定一个任意嵌套的列表结构,flatten
将返回一个新的包含所有可以通过该列表结构到达的原子的列表。如果我们把列表结构看成是
一棵树,那么 flatten 将返回该树中所有叶子的列表。如果这棵树代表 lisp 代码,通过检查
表达式中某些对象的存在,flatten 完成了一种代码遍历(code-walking),这是本书中
反复出现的主题。

  1. (defun fact (x)
  2. (if (= x 0)
  3. 1
  4. (* x (fact (- x 1)))))
  5. (defun choose (n r)
  6. (/ (fact n)
  7. (fact (- n r))
  8. (fact r)))

factchoose 是阶乘和二项式系数函数的显示实现。

1.4 许可证

因为我相信藏在本书代码背后的概念就像物理观察或数学证明一样基本,所以即使我想,
我也不相信我可以拥有它们的所有权。因此,你可以自由地使用本书的代码。下面是
随代码分发的非常自由的许可证:

  1. ;; This is the source code for the book
  2. ;; _Let_Over_Lambda_ by Doug Hoyte <[doug@hoytech.com](mailto:doug@hoytech.com)>
  3. .
  4. ;; This code is (C) 2002-2008, Doug Hoyte <[doug@hoytech.com](mailto:doug@hoytech.com)>
  5. .
  6. ;;
  7. ;; You are free to use, modify, and re-distribute
  8. ;; this code however you want, except that any
  9. ;; modifications must be clearly indicated before
  10. ;; re-distribution. There is no warranty,
  11. ;; expressed nor implied.
  12. ;;
  13. ;; Attribution of this code to me, Doug Hoyte <[doug@hoytech.com](mailto:doug@hoytech.com)>
  14. , is
  15. ;; appreciated but not necessary. If you find the
  16. ;; code useful, or would like documentation,
  17. ;; Please consider buying the book!

The text of this book is (C) 2008 Doug Hoyte [doug@hoytech.com](mailto:doug@hoytech.com)
. All rights reserved.

1.5 致谢

感谢 Brian Hoyte [doug@hoytech.com](mailto:doug@hoytech.com), Nancy Holmes, Rosalie Holmes,
Ian, Alex, all the rest of my family; syke, madness, fyodor, cyb0rg/asm, theclone, blackheart,
d00tz, rt, magma, nummish, zhivago, defrost; Mike Conroy, Sylvia Russell, Alan Paeth, Rob
McArthur, Sylvie Desjardins, John McCarthy, Paul Graham, Donald Knuth, Leo Brodie, Bruce
Schneier, Richard Stallman, Edi Weitz, Peter Norvig, Peter Seibel, Christian Queinnec, Keith
Bostic, John Gamble; the designers and creators of COMMON LISP, especially Guy Steele, Richard
Gabriel, and Kent Pitman, the developers and maintainers of CMUCL/SBCL, CLISP, OpenBSD, GNU/Linux.

特别感谢 Ian Hoyte [doug@hoytech.com](mailto:doug@hoytech.com) 为本书设计封面及 Leo Brodie 设计背面.

本书献给所有爱编程的人。