Lisp的本质(2006)

2020-10-26 17:31:08

当我第一次在Web的各个角落偶然发现Lisp倡导时,我已经是一名经验丰富的程序员了。在那件事上 在这一点上,我已经领会了在当时看起来范围很广的编程语言。我很自豪拥有常见的嫌疑犯(C++、Java、C#、 等)。在我的服役记录上,我的印象是我知道关于编程语言的一切。我不可能 可能更大错特错了。

当我看到一些示例代码时,我最初学习Lisp的尝试就戛然而止。我想我也有同样的想法 几千个曾经站在我的立场上的人的心思:";究竟为什么会有人想要用这样一种语言呢? 可怕的语法?如果一门语言的创造者不愿费心给它一个令人愉快的语法,我就不会费心去学习它。毕竟,我几乎被臭名昭著的Lisp括号弄瞎了眼睛!

在我恢复视力的那一刻,我就把我的挫败感告诉了一些Lisp教派的成员。几乎立刻我就遭到了 一组标准的响应:lisp的括号只是一个表面的问题,Lisp有一个巨大的好处,代码和数据 以同样的方式表示(显然,这是对XML的巨大改进),Lisp具有非常强大的元编程功能 允许程序编写代码和修改自身的工具,Lisp允许创建特定于 手头的问题是,Lisp模糊了运行时和编译时之间的区别,Lisp、Lisp、Lisp……。这份名单令人印象深刻。不用说,所有这些都说不通。没有人能用具体的例子来说明这些功能的用处,因为这些 技术应该只在大型软件系统中有用。在对传统编程语言进行了长达数小时的辩论之后 把工作做好,我放弃了。我不打算花几个月的时间来学习一门语法很糟糕的语言,以便 了解没有有用示例的晦涩难懂的功能。我的时代还没有到来。

几个月来,Lisp的倡导者一直在坚持。我被难住了。我认识并非常尊敬的许多非常聪明的人都是 用近乎宗教的奉献来赞扬Lisp。一定有什么东西在那里,一些我拿不到的东西! 最终,我对知识的渴求赢得了我的心。我冒了险,咬紧牙关,弄脏了双手,开始了几个月的思考 弯曲练习。这是一次在无尽的挫折之湖上的旅行。我把脑子里的东西翻过来,把它冲洗干净,然后放回去 地点。我经历了七个地狱环,然后回来了。然后我就明白了。

顿悟是瞬间到来的。前一刻我什么都不懂,下一刻一切都理所当然了。我有过这样的经历。 实现了涅槃。我几十次听到不同的人引用埃里克·雷蒙德的话:Lisp值得学习 当您最终获得它时,您将拥有深刻的启蒙经验;这些经验将使您成为一名更好的程序员 剩下的时间,即使你从来不经常使用Lisp本身。";我从来没有理解过这句话。我从来不相信这会是真的 是真的。终于,在经历了所有的痛苦之后,这一切都变得有意义了!这件事的真相比我想象的要多得多。我已经取得了一项成就 一种近乎神圣的精神状态,一次瞬间的启蒙经历,在不到一个月的时间里就彻底改变了我对计算机科学的看法 一秒钟。

就在那一刻,我成为了Lisp邪教的一员。我感觉到一种忍术大师一定会有的感觉:我必须把我的新发现 在我的有生之年,至少有十个迷失的灵魂获得了知识。我走的是平常的路。我在重复同样的论点, 多年来一直给我(只是现在它们才真正有意义!),希望能让毫无戒心的旁观者改变主意。它没有起作用。我的 坚持不懈激发了一些人的兴趣,但仅仅看到示例Lisp代码,他们的好奇心就会减弱。也许几年来 倡导将锻造一些新的里斯珀斯,但我并不满意。一定有更好的办法。

我仔细考虑了这件事。Lisp有没有什么固有的硬性,阻碍了非常聪明、有经验的 程序员理解不了吗?不,没有。毕竟,我做到了,如果我能做到,任何人都能做到。那是什么让你 口齿不清就这么难懂吗?像往常一样,答案出乎意料地出现了。当然了!。教任何人任何涉及到的事情 在他们已经理解的概念基础上构建高级概念!如果过程变得有趣,事情得到解释 正确地说,新概念会变得和帮助他们理解的原始构件一样直观。这就是问题所在! 元程序设计、代码和数据合二为一、自修改程序、领域特定m

千里之行,始于足下。启蒙之旅也不例外,我们的第一步只是碰巧 XML。关于XML,还有什么可以说的还没有说过呢?事实证明,这是相当多的。当这里什么都没有的时候 XML本身特别有趣,它与Lisp的关系非常吸引人。XML是Lisp非常熟悉的概念 倡导者需要的东西太多了。它是我们向普通程序员传达理解的桥梁。所以让我们复活这匹死马,干掉它 大棒,并冒险进入XML的荒野,在我们之前,没有人敢冒险进入这片荒野。是时候从我们熟悉的地方看到这轮太熟悉的月亮了。 另一边。

从表面上看,XML只不过是一种标准化语法,用于以人类可读的形式表示任意的分层数据。待办事项列表、网页、医疗记录、汽车保险索赔、配置文件都是XML潜在用途的示例。让我们使用吧 举个简单的待办事项清单为例(在几个部分中,你会从一个全新的角度来看待它):

<;待办事项名称=";家务";>; <;项目优先级=";高>;打扫房间。<;/项目>; <;项目优先级=";中等>;洗碗。<;/项目>; <;项目优先级=";中等>;购买更多肥皂。<;/项目>; <;/待办事项>;

如果我们在这个待办事项列表中释放我们最喜欢的XML解析器,会发生什么呢?一旦数据被解析,它在内存中是如何表示的呢?这个 当然,最自然的表示是树--分层数据的完美数据结构。说了这么多,做了这么多之后,XML是 实际上只是一棵树,序列化成人类可读的形式。任何可以用树表示的东西都可以用XML表示,反之亦然 反之亦然。我希望你能理解这个想法。下一步是什么,这一点非常重要。

让我们再深入一点。通常将哪些其他类型的数据表示为树?在这一点上,这份清单就像 无限的,所以我会给你一个提示,我要说的是--试着记住你以前的编译器课程。如果你有一个模糊的记忆 该源代码在解析后存储在树中,您就走上了正确的道路。任何编译器都不可避免地解析源代码 转换为抽象语法树。这并不奇怪,因为源代码是分层的:函数包含参数和代码块。代码块包含表达式和语句。表达式包含变量和运算符。事情就这样过去了。

让我们将我们的推论应用到这个想法上,即任何树都可以很容易地序列化为XML。如果最终表示了所有源代码 作为一棵树,任何一棵树都可以序列化为XML,那么所有的源代码都可以转换成XML,对吗?让我们来说明一下这一点。 一个简单的例子说明了有趣的属性。请考虑以下函数:

您能将此函数定义转换为它的XML等效项吗?事实证明,这相当简单。当然,有很多方法可以 这么做吧。以下是生成的XML的一种形式:

<;Define-Function Return-type=";int";name=";add";>; <;参数>; <;参数类型=";int";>;arg1<;/参数>; <;参数类型=";int";>;arg2<;/参数>; <;/参数>; <;正文>; <;退货>; <;ADD VALUE1=";Arg1";value2=";arg2";/>; <;/退货>; <;/Body>; <;/定义>;

我们可以用任何语言完成这个相对简单的练习。我们可以将任何源代码转换为XML,并且可以将 生成的XML返回到原始源代码。我们可以编写一个将Java转换为XML的转换器,也可以编写一个将XML转换回XML的转换器 爪哇。我们可以为C++做同样的事情。(如果你想知道是否有人疯狂到这样做,看看GCC-xml吧)。此外,对于共享共同特征但使用不同语法的语言(这在某种程度上对大多数主流是正确的 语言),我们可以使用XML作为中间表示将源代码从一种语言转换为另一种语言。我们可以用我们的 Java2XML转换器将Java程序转换为XML。然后,我们可以对生成的XML运行XML2CPP转换器,并将其转换为 C++代码。幸运的是(如果我们避免使用C++中不存在的Java特性),我们会得到一个可以工作的C++程序。很整洁,是吧?

所有这些有效地意味着我们可以使用XML进行源代码的泛型存储。我们将能够创建一个完整的类 使用统一语法的编程语言,以及编写将现有源代码转换为XML的转换器。如果我们要 实际上采用了这个想法,不同语言的编译器不需要为他们特定的语法实现解析器-他们 只需使用XML解析器将XML直接转换为抽象语法树即可。

到目前为止,您可能想知道为什么我要开始XML十字军东征,它与Lisp有什么关系(毕竟,Lisp就是创建的 大约在XML之前30年)。我保证一切很快就会明朗起来。但在我们迈出第二步之前,让我们先走一步 通过一个小的哲学练习。仔细看一下上面的";add";函数的XML版本。你会怎么做 把它归类吗?是数据还是代码?如果您仔细考虑一下,就会意识到有很好的理由将此XML 这两个类别的代码片段。它是XML,它只是以标准化格式编码的信息。我们已经确定了 可以从内存中的树数据结构生成(这实际上就是GCC-xml所做的事情)。它躺在一个文件里,没有 显然是执行它的方式。我们可以将其解析为XML节点树,并对其进行各种转换。它的数据。但请稍等片刻 等一下!说完和做完之后,用不同语法编写的函数都是一样的,对吗?一旦解析,它的树可能是 输入到编译器中,我们就可以执行它。我们可以很容易地为这段XML代码编写一个小型解释器,然后执行它 直接去吧。或者,我们可以将其转换为Java或C++代码,编译并运行。这是它的代码。

那么,我们说到哪了?看起来我们刚刚到了一个有趣的地步。一个传统上很难理解的概念 现在变得惊人的简单和直观。代码也总是数据!这是否意味着数据也总是代码?这听起来很疯狂 情况很可能是这样。还记得我怎么保证你会以全新的眼光看待我们的待办事项清单吗?让我重申一下 那个承诺。但是我们现在还没有准备好讨论这个问题。现在,让我们继续沿着我们的道路走下去。

前面我提到过,我们可以很容易地编写解释器来执行Add函数的XML片段。当然是这个了 听起来像是纯粹的理论练习。哪个心智正常的人会出于实际目的去做那件事呢?嗯,事实证明 相当多的人会不同意。在你的职业生涯中,你可能也至少遇到过并使用过他们的作品一次。我有你吗? 坐在你的座位边上吗?如果是这样的话,让我们继续前进吧!

既然我们已经到月球的黑暗面旅行了,那我们就别走了。我们仍然可以通过探索它来学到一些东西 再多一点,让我们再往前走一步。首先,我们闭上眼睛,回忆起2000年冬天的一个寒冷的雨夜。一个 一位名叫詹姆斯·邓肯·戴维森1号的著名开发者 Tomcat Servlet容器。到了构建更改的时候,他小心翼翼地保存了所有文件 然后跑了Make。错误。很多错误。有点不对劲。经过仔细检查,詹姆斯惊呼道:我的命令没有执行吗? 因为我的账单前面有空格?!";确实,这就是问题所在。再来一次。詹姆斯已经受够了。他能透过它感觉到满月。 云彩让他充满了冒险精神。他创建了一个全新的Java项目,并迅速将一个简单但非常有用的 实用程序。这位天才的火花使用Java属性文件来获取有关如何构建项目的信息。詹姆斯现在可以写出等同的东西了。 Makefile的一个很好的格式,再也不用担心该死的空格了。他的公用事业公司使出了浑身解数。

上面的代码片段将源目录复制到目标目录。Ant定位一个复制任务(实际上是一个Java类),设置 通过调用适当的Java方法获得适当的参数(todir和fileset),然后执行任务。蚂蚁自带了一套 核心任务,任何人只需编写遵循特定约定的Java类,就可以用他们自己的任务来扩展它。蚂蚁发现 这些类,并在遇到具有适当名称的XML元素时执行它们。很简单。有效的蚂蚁 实现了我们在上一节中讨论的内容:它充当使用XML作为语法的语言的解释器 通过将XML元素转换为适当的Java指令。我们可以编写一个添加任务,并让Ant在遇到它时执行它 上一节中提供的用于添加的XML片段!考虑到Ant是一个非常受欢迎的项目, 在上一节中介绍的内容开始看起来更理智了。毕竟,它们每天都被用在可能相当于 数以千计的公司!

到目前为止,我还没有说过为什么Ant要不辞辛劳地解释XML。不要试图在网上寻找答案 它的网站也不一样--你会发现没有任何有价值的东西。反正跟我们的讨论没什么关系。让我们再走一步吧。现在是时候 找出原因。

有时候,正确的决定是在没有全意识的情况下做出的。 了解所有涉及的问题。我不确定詹姆斯是否 我知道他为什么选择XML--这很可能是一个下意识的决定。 至少,我在蚂蚁网站上看到的使用 XML都是错误的原因。似乎主要关注的是 围绕可移植性和可扩展性展开。我不明白为什么 在Ant的案例中,XML有助于推进这些目标。什么是 相对于简单的Java源代码,使用解释型XML的优势是什么? 为什么不创建一组具有良好API的类,用于通常 使用的任务(复制目录、编译等)。并使用 那些直接来自Java源代码的吗?这将在每个 运行Java的平台(这是Ant无论如何都需要的),它的 无限的可扩展性,它的好处是拥有更多 令人愉快,熟悉的语法。那么为什么是XML呢?我们能找到一个很好的理由吗? 因为你用了它?

事实证明,我们可以(尽管如我前面提到的,我可以 不确定詹姆斯是否有意识地意识到了这一点)。XML具有 在引入方面要灵活得多的特性 语义结构比Java所希望的要好。不要这样做 担心,我不会落入使用大话的陷阱 描述难以理解的概念。这实际上是一个 相对简单的想法,尽管可能需要一些努力才能 解释一下。系好安全带。我们即将进行一次巨大的飞跃 向着实现涅槃的方向前进。

我们如何用Java代码表示上面的副本示例呢?这里有一种方法可以做到这一点:

代码几乎相同,只是比原始的XML稍长一些。所以 有什么不同?答案是XML片段引入了一个 用于复制的特殊语义结构。如果我们可以用Java做这件事,它看起来 就像这样:

你能看出不同之处吗?上面的代码(如果可能的话 在Java中)是用于复制文件的特殊操作符-类似于 For循环或Java5中引入的新foreach结构。 如果有一个从XML到Java的自动转换器,很可能 制造上述胡言乱语。原因是Java 接受的语法树语法由语言固定 规格-我们无法修改它。我们可以添加 包、类、方法,但是我们不能扩展Java来制作 可以添加新的操作员。但是我们可以对我们的 核心在XML中的内容-其语法树不受限制 除了我们的翻译什么都行!如果这个想法还不清楚, 考虑在Java中引入特殊运算符,除非:

在前两个示例中,我们对Java语言进行了扩展,以引入 用于复制文件的运算符和条件运算符,除非

.