The Nature of Lisp
Lisp的本质
Monday, May 8, 2006
2006 年 5 月 8 日,星期一
Introduction 介绍
When I first stumbled into Lisp advocacy on various corners of the web I was already an experienced programmer. At that point I had grokked what seemed at the time a wide range of programming languages. I was proud to have the usual suspects (C++, Java, C#, etc.) on my service record and was under impression that I knew everything there is to know about programming languages. I couldn't have possibly been more wrong.
当我第一次偶然接触到 Lisp 在网络各个角落的宣传时,我已经是一个有经验的程序员了。那时,我已经摸索了当时似乎各种各样的编程语言。我很自豪地在我的服务记录中拥有通常的嫌疑人(C++,Java,C#等),并且印象中我知道有关编程语言的所有知识。我错得不能再错了。
My initial attempt to learn Lisp came to a crashing halt as soon as I saw some sample code. I suppose the same thought ran through my mind that ran through thousands of other minds who were ever in my shoes: "Why on Earth would anyone want to use a language with such horrific syntax?!" I couldn't be bothered to learn a language if its creators couldn't be bothered to give it a pleasant syntax. After all, I was almost blinded by the infamous Lisp parentheses!
我最初学习 Lisp 的尝试在我看到一些示例代码后就戛然而止了。我想同样的想法在我的脑海中闪过,也闪过了成千上万曾经站在我的立场上的其他思想:“为什么有人愿意使用一种语法如此可怕的语言?!如果一门语言的创造者不愿意给它一个愉快的语法,我就懒得去学习它。毕竟,我几乎被臭名昭著的 Lisp 括号蒙蔽了双眼!
The moment I regained my sight I communicated my frustrations to some members of the Lisp sect. Almost immediately I was bombarded by a standard set of responses: Lisp's parentheses are only a superficial matter, Lisp has a huge benefit of code and data being expressed in the same manner (which, obviously, is a huge improvement over XML), Lisp has tremendously powerful metaprogramming facilities that allow programs to write code and modify themselves, Lisp allows for creation of mini-languages specific to the problem at hand, Lisp blurs the distinction between run time and compile time, Lisp, Lisp, Lisp... The list was very impressive. Needless to say none of it made sense. Nobody could illustrate the usefulness of these features with specific examples because these techniques are supposedly only useful in large software systems. After many hours of debating that conventional programming languages do the job just fine, I gave up. I wasn't about to invest months into learning a language with a terrible syntax in order to understand obscure features that had no useful examples. My time has not yet come.
在我恢复视力的那一刻,我向Lisp教派的一些成员表达了我的沮丧。几乎立刻,我就被一组标准的回答轰炸了:Lisp的括号只是一个表面的问题,Lisp有一个巨大的好处,即代码和数据以相同的方式表达(显然,这是对XML的巨大改进),Lisp具有非常强大的元编程工具,允许程序编写代码并修改自己。 Lisp允许创建特定于手头问题的迷你语言,Lisp模糊了运行时和编译时之间的区别,Lisp,Lisp,Lisp......这份名单非常令人印象深刻。毋庸置疑,这一切都没有意义。没有人能用具体的例子来说明这些功能的有用性,因为这些技术被认为只在大型软件系统中有用。经过几个小时的争论,传统的编程语言可以很好地完成这项工作,我放弃了。我不打算花几个月的时间去学习一门语法很糟糕的语言,以便理解那些没有有用例子的晦涩难懂的功能。我的时代还没有到来。
For many months the Lisp advocates pressed on. I was baffled. Many extremely intelligent people I knew and had much respect for were praising Lisp with almost religious dedication. There had to be something there, something I couldn't afford not to get my hands on! Eventually my thirst for knowledge won me over. I took the plunge, bit the bullet, got my hands dirty, and began months of mind bending exercises. It was a journey on an endless lake of frustration. I turned my mind inside out, rinsed it, and put it back in place. I went through seven rings of hell and came back. And then I got it.
几个月来,Lisp的拥护者们一直在努力。我很困惑。我认识并非常尊敬的许多非常聪明的人都以近乎宗教的奉献精神赞美Lisp。那里一定有什么东西,我不能不动手的东西!最终,我对知识的渴望征服了我。我冒险,咬紧牙关,弄脏了我的手,开始了几个月的脑洞大开的练习。这是在无尽的挫折之湖上的旅程。我把脑子里里翻了个底朝天,冲洗了一下,然后把它放回原位。我经历了地狱的七个环,然后回来了。然后我明白了。
The enlightenment came instantaneously. One moment I understood nothing, and the next moment everything clicked into place. I've achieved nirvana. Dozens of times I heard Eric Raymond's statement quoted by different people: "Lisp is worth learning for the profound enlightenment experience you will have when you finally get it; that experience will make you a better programmer for the rest of your days, even if you never actually use Lisp itself a lot." I never understood this statement. I never believed it could be true. And finally, after all the pain, it made sense! There was more truth to it than I ever could have imagined. I've achieved an almost divine state of mind, an instantaneous enlightenment experience that turned my view of computer science on its head in less than a single second.
启蒙瞬间到来。前一刻我什么都不懂,下一刻一切都咔嚓一声就到位了。我已经实现了涅槃。我数十次听到不同的人引用埃里克·雷蒙德(Eric Raymond)的一句话:“Lisp值得学习,因为当你最终得到它时,你将拥有深刻的启蒙体验;这种经历将使你在余下的日子里成为一个更好的程序员,即使你从未真正使用过Lisp本身。我从来不明白这句话。我从来不相信这是真的。最后,在经历了所有的痛苦之后,这是有道理的!它比我想象的要多得多。我达到了一种近乎神圣的心态,一种瞬间的启蒙体验,在不到一秒钟的时间内改变了我对计算机科学的看法。
That very second I became a member of the Lisp cult. I felt something a ninjitsu master must feel: I had to spread my newfound knowledge to at least ten lost souls in the course of my lifetime. I took the usual path. I was rehashing the same arguments that were given to me for years (only now they actually made sense!), hoping to convert unsuspecting bystanders. It didn't work. My persistence sparked a few people's interest but their curiosity dwindled at the mere sight of sample Lisp code. Perhaps years of advocacy would forge a few new Lispers, but I wasn't satisfied. There had to be a better way.
就在那一刻,我成为了Lisp邪教的成员。我感受到了一个忍术大师必须感受到的东西:在我有生之年,我必须将我新发现的知识传播给至少十个迷失的灵魂。我走了通常的路。我一直在重复多年来给我的相同论点(只是现在它们真的有意义了!),希望能改变毫无戒心的旁观者。它没有用。我的坚持激起了一些人的兴趣,但他们的好奇心在看到Lisp代码样本时就消失了。也许多年的倡导会造就一些新的Lispers,但我并不满意。必须有更好的方法。
I gave the matter careful thought. Is there something inherently hard about Lisp that prevents very intelligent, experienced programmers from understanding it? No, there isn't. After all, I got it, and if I can do it, anybody can. Then what is it that makes Lisp so hard to understand? The answer, as such things usually do, came unexpectedly. Of course! Teaching anybody anything involves building advanced concepts on top of concepts they already understand! If the process is made interesting and the matter is explained properly the new concepts become as intuitive as the original building blocks that aided their understanding. That was the problem! Metaprogramming, code and data in one representation, self-modifying programs, domain specific mini-languages, none of the explanations for these concepts referenced familiar territory. How could I expect anyone to understand them! No wonder people wanted specific examples. I could as well have been speaking in Martian!
我仔细考虑了一下。Lisp 是否有什么固有的困难阻碍了非常聪明、经验丰富的程序员理解它?不,没有。毕竟,我做到了,如果我能做到,任何人都可以做到。那么,是什么让Lisp如此难以理解呢?答案,就像通常的事情一样,出乎意料地来了。答案是肯定的!教任何人任何东西都需要在他们已经理解的概念之上构建高级概念!如果这个过程变得有趣,并且问题得到适当的解释,那么新概念就会变得像帮助他们理解的原始构建块一样直观。这就是问题所在!元编程、代码和数据在一个表示形式中、自我修改程序、特定领域的迷你语言,对这些概念的解释都没有引用熟悉的领域。我怎么能指望有人能理解他们!难怪人们想要具体的例子。我还不如用火星语说话!
I shared my ideas with fellow Lispers. "Well, of course these concepts aren't explained in terms of familiar territory", they said. "They are so different, they're unlike anything these people have learned before." This was a poor excuse. "I do not believe this to be true", I said. The response was unanimous: "Why don't you give it a try?" So I did. This article is a product of my efforts. It is my attempt to explain Lisp in familiar, intuitive concepts. I urge brave souls to read on. Grab your favorite drink. Take a deep breath. Prepare to be blown away. Oh, and may the Force be with you.
我与其他 Lispers 分享了我的想法。“嗯,当然,这些概念不是用熟悉的领域来解释的,”他们说。“他们是如此不同,他们与这些人以前学到的任何东西都不一样。这是一个糟糕的借口。“我不相信这是真的,”我说。得到的回答是一致的:“你为什么不试一试呢?所以我做到了。这篇文章是我努力的产物。我试图用熟悉的、直观的概念来解释Lisp。我敦促勇敢的灵魂继续阅读。拿起你最喜欢的饮料。深吸一口气。准备好被吹走。哦,愿原力与你同在。
XML Reloaded XML 重新加载
A thousand mile journey starts with a single step. A journey to enlightenment is no exception and our first step just happens to be XML. What more could possibly be said about XML that hasn't already been said? It turns out, quite a bit. While there's nothing particularly interesting about XML itself, its relationship to Lisp is fascinating. XML is the all too familiar concept that Lisp advocates need so much. It is our bridge to conveying understanding to regular programmers. So let's revive the dead horse, take out the stick, and venture into XML wilderness that no one dared venture into before us. It's time to see the all too familiar moon from the other side.
千里之行,始于足下。启蒙之旅也不例外,我们的第一步恰好是 XML。关于XML,还有什么可能说的呢?事实证明,相当多。虽然XML本身并没有什么特别有趣的地方,但它与Lisp的关系却令人着迷。XML 是 Lisp 倡导者非常需要的熟悉概念。它是我们向普通程序员传达理解的桥梁。因此,让我们复活死马,拿出棍子,冒险进入在我们之前没有人敢冒险进入的 XML 荒野。是时候从另一边看到太熟悉的月亮了。
Superficially XML is nothing more than a standardized syntax used to express arbitrary hierarchical data in human readable form. To-do lists, web pages, medical records, auto insurance claims, configuration files are all examples of potential XML use. Let's use a simple to-do list as an example (in a couple of sections you'll see it in a whole new light):
从表面上看,XML只不过是一种标准化语法,用于以人类可读的形式表示任意分层数据。待办事项列表、网页、医疗记录、汽车保险索赔、配置文件都是潜在 XML 使用的示例。让我们以一个简单的待办事项列表为例(在几个部分中,您将以全新的眼光看待它):
<todo name="housework">
<item priority="high">Clean the house.</item>
<item priority="medium">Wash the dishes.</item>
<item priority="medium">Buy more soap.</item>
</todo>
What happens if we unleash our favorite XML parser on this to-do list? Once the data is parsed, how is it represented in memory? The most natural representation is, of course, a tree - a perfect data structure for hierarchical data. After all is said and done, XML is really just a tree serialized to a human readable form. Anything that can be represented in a tree can be represented in XML and vice versa. I hope you understand this idea. It's very important for what's coming next.
如果我们在这个待办事项列表中释放我们最喜欢的 XML 解析器会发生什么?解析数据后,如何在内存中表示数据?当然,最自然的表示形式是树 - 分层数据的完美数据结构。总而言之,XML 实际上只是一个序列化为人类可读形式的树。任何可以在树中表示的东西都可以用 XML 表示,反之亦然。我希望你能理解这个想法。这对接下来的事情非常重要。
Let's take this a little further. What other type of data is often represented as a tree? At this point the list is as good as infinite so I'll give you a hint at what I'm getting at - try to remember your old compiler course. If you have a vague recollection that source code is stored in a tree after it's parsed, you're on the right track. Any compiler inevitably parses the source code into an abstract syntax tree. This isn't surprising since source code is hierarchical: functions contain arguments and blocks of code. Blocks of code contain expressions and statements. Expressions contain variables and operators. And so it goes.
让我们更进一步。哪些其他类型的数据通常表示为树?在这一点上,列表和无限一样好,所以我会给你一个提示,说明我正在得到什么 - 试着记住你的旧编译器课程。如果你模糊地记得源代码在解析后存储在树中,那么你就走在正确的轨道上。任何编译器都不可避免地将源代码解析为抽象语法树。这并不奇怪,因为源代码是分层的:函数包含参数和代码块。代码块包含表达式和语句。表达式包含变量和运算符。事情就是这样。
Let's apply our corollary that any tree can easily be serialized into XML to this idea. If all source code is eventually represented as a tree, and any tree can be serialized into XML, then all source code can be converted to XML, right? Let's illustrate this interesting property by a simple example. Consider the function below:
让我们将我们的推论应用到这个想法中,即任何树都可以很容易地序列化为 XML。如果所有的源代码最终都表示为一棵树,并且任何一棵树都可以序列化为XML,那么所有的源代码都可以转换为XML,对吗?让我们通过一个简单的例子来说明这个有趣的属性。请考虑以下功能:
int add(int arg1, int arg2)
{
return arg1 + arg2;
}
Can you convert this function definition to its XML equivalent? Turns out, it's reasonably simple. Naturally there are many ways to do this. Here is one way the resulting XML can look like:
是否可以将此函数定义转换为其 XML 等效项?事实证明,这相当简单。当然,有很多方法可以做到这一点。生成的 XML 的外观如下:
<define-function return-type="int" name="add">
<arguments>
<argument type="int">arg1</argument>
<argument type="int">arg2</argument>
</arguments>
<body>
<return>
<add value1="arg1" value2="arg2" />
</return>
</body>
</define>
We can go through this relatively simple exercise with any language. We can turn any source code into XML, and we can transform the resulting XML back to original source code. We can write a converter that turns Java into XML and a converter that turns XML back to Java. We could do the same for C++. (In case you're wondering if anyone is crazy enough to do it, take a look at GCC-XML). Furthermore, for languages that share common features but use different syntax (which to some extent is true about most mainstream languages) we could convert source code from one language to another using XML as an intermediary representation. We could use our Java2XML converter to convert a Java program to XML. We could then run an XML2CPP converter on the resulting XML and turn it into C++ code. With any luck (if we avoid using features of Java that don't exist in C++) we'll get a working C++ program. Neat, eh?
我们可以用任何语言完成这个相对简单的练习。我们可以将任何源代码转换为XML,并且可以将生成的XML转换回原始源代码。我们可以编写一个将 Java 转换为 XML 的转换器,以及一个将 XML 转换回 Java 的转换器。我们可以对C++做同样的事情。(如果你想知道是否有人疯狂到可以这样做,请查看 GCC-XML)。此外,对于具有共同特征但使用不同语法的语言(在某种程度上,大多数主流语言都是如此),我们可以使用XML作为中介表示将源代码从一种语言转换为另一种语言。我们可以使用我们的 Java2XML 转换器将 Java 程序转换为 XML。然后,我们可以在生成的 XML 上运行一个XML2CPP转换器,并将其转换为C++代码。如果运气好的话(如果我们避免使用C++中不存在的Java功能),我们将得到一个有效的C++程序。整洁,嗯?
All this effectively means that we can use XML for generic storage of source code. We'd be able to create a whole class of programming languages that use uniform syntax, as well as write transformers that convert existing source code to XML. If we were to actually adopt this idea, compilers for different languages wouldn't need to implement parsers for their specific grammars - they'd simply use an XML parser to turn XML directly into an abstract syntax tree.
所有这些都有效地意味着我们可以使用XML来存储源代码的通用存储。我们将能够创建一整类使用统一语法的编程语言,以及编写将现有源代码转换为 XML 的转换器。如果我们真的采用这个想法,不同语言的编译器就不需要为其特定的语法实现解析器——它们只需使用 XML 解析器将 XML 直接转换为抽象语法树。
By now you're probably wondering why I've embarked on the XML crusade and what it has to do with Lisp (after all, Lisp was created about thirty years before XML). I promise that everything will become clear soon enough. But before we take our second step, let's go through a small philosophical exercise. Take a good look at the XML version of our "add" function above. How would you classify it? Is it data or code? If you think about it for a moment you'll realize that there are good reasons to put this XML snippet into both categories. It's XML and it's just information encoded in a standardized format. We've already determined that it can be generated from a tree data structure in memory (that's effectively what GCC-XML does). It's lying around in a file with no apparent way to execute it. We can parse it into a tree of XML nodes and do various transformations on it. It's data. But wait a moment! When all is said and done it's the same "add" function written with a different syntax, right? Once parsed, its tree could be fed into a compiler and we could execute it. We could easily write a small interpreter for this XML code and we could execute it directly. Alternatively, we could transform it into Java or C++ code, compile it, and run it. It's code.
到现在为止,您可能想知道我为什么要开始XML运动,以及它与Lisp有什么关系(毕竟,Lisp是在XML之前大约三十年创建的)。我保证一切都会很快变得清晰。但在我们迈出第二步之前,让我们先进行一个小小的哲学练习。好好看看上面的“add”函数的 XML 版本。你会如何分类?是数据还是代码?如果你仔细想一想,你就会意识到,有充分的理由将这个XML片段归入这两个类别。它是 XML,它只是以标准化格式编码的信息。我们已经确定它可以从内存中的树状数据结构生成(这实际上是GCC-XML所做的)。它躺在一个文件中,没有明显的方法来执行它。我们可以将其解析为XML节点树,并对其进行各种转换。这是数据。但是等一下!当一切都说完了,它是用不同的语法编写的同一个“add”函数,对吧?一旦解析,它的树就可以被输入到编译器中,我们可以执行它。我们可以很容易地为这个XML代码编写一个小型的解释器,我们可以直接执行它。或者,我们可以将其转换为 Java 或 C++ 代码,编译并运行它。这是代码。
So, where are we? Looks like we've just arrived to an interesting point. A concept that has traditionally been so hard to understand is now amazingly simple and intuitive. Code is also always data! Does it mean that data is also always code? As crazy as this sounds this very well might be the case. Remember how I promised that you'll see our to-do list in a whole new light? Let me reiterate on that promise. But we aren't ready to discuss this just yet. For now let's continue walking down our path.
那么,我们在哪里?看起来我们刚刚到达了一个有趣的点。一个传统上难以理解的概念现在变得非常简单和直观。代码也永远是数据!这是否意味着数据也始终是代码?尽管这听起来很疯狂,但情况可能确实如此。还记得我曾承诺过你会以全新的眼光看待我们的待办事项清单吗?请允许我重申这一承诺。但我们还没有准备好讨论这个问题。现在,让我们继续走我们的路。
A little earlier I mentioned that we could easily write an interpreter to execute our XML snippet of the add function. Of course this sounds like a purely theoretical exercise. Who in their right mind would want to do that for practical purposes? Well, it turns out quite a few people would disagree. You've likely encountered and used their work at least once in your career, too. Do I have you out on the edge of your seat? If so, let's move on!
前面我提到过,我们可以很容易地编写一个解释器来执行 add 函数的 XML 代码片段。当然,这听起来像是纯粹的理论练习。谁会出于实际目的而这样做呢?好吧,事实证明很多人会不同意。在你的职业生涯中,你可能也至少遇到过一次,也至少使用过一次他们的工作。我让你坐在座位的边缘吗?如果是这样,让我们继续吧!
Ant Reloaded 蚂蚁重装上阵
Now that we've made the trip to the dark side of the moon, let's not leave quite yet. We may still learn something by exploring it a little more, so let's take another step. We begin by closing our eyes and remembering a cold rainy night in the winter of 2000. A prominent developer by the name of James Duncan Davidson1 was hacking his way through Tomcat servlet container. As the time came to build the changes he carefully saved all his files and ran make. Errors. Lots of errors. Something was wrong. After careful examination James exclaimed: "Is my command not executing because I have a space in front of my tab?!" Indeed, this was the problem. Again. James has had enough. He could sense the full moon through the clouds and it made him adventurous. He created a fresh Java project and quickly hacked together a simple but surprisingly useful utility. This spark of genius used Java property files for information on how to build the project. James could now write the equivalent of the makefile in a nice format without worrying about the damned spaces ever again. His utility did all the hard work by interpreting the property file and taking appropriate actions to build the project. It was neat. Another Neat Tool. Ant.
现在我们已经前往月球的黑暗面,让我们先不要离开。我们可能仍然可以通过多探索一下来学到一些东西,所以让我们再迈出一步。我们首先闭上眼睛,想起2000年冬天一个寒冷的雨夜。一位名叫 James Duncan Davidson 的著名开发人员 1 正在破解 Tomcat servlet 容器。当需要构建更改时,他小心翼翼地保存了所有文件并运行了 make。错误。很多错误。有些不对劲。经过仔细检查,詹姆斯惊呼道:“我的命令没有执行,是因为我的标签前面有一个空格吗?!事实上,这就是问题所在。再。詹姆斯受够了。他能透过云层感觉到满月,这让他变得冒险。他创建了一个新的 Java 项目,并迅速将一个简单但非常有用的实用程序组合在一起。这个天才的火花使用 Java 属性文件来获取有关如何构建项目的信息。James现在可以用一个很好的格式写出相当于makefile的东西,而不用再担心该死的空格了。他的实用程序通过解释属性文件并采取适当的措施来构建项目,从而完成了所有艰苦的工作。很整洁。另一个简洁的工具。蚂蚁。
After using Ant to build Tomcat for a few months it became clear that Java property files are not sufficient to express complicated build instructions. Files needed to be checked out, copied, compiled, sent to another machine, and unit tested. In case of failure e-mails needed to be sent out to appropriate people. In case of success "Bad to the Bone" needed to be played at the highest possible volume. At the end of the track volume had to be restored to its original level. Yes, Java property files didn't cut it anymore. James needed a more flexible solution. He didn't feel like writing his own parser (especially since he wanted an industry standard solution). XML seemed like a reasonable alternative. In a couple of days Ant was ported to XML. It was the best thing since sliced bread.
在使用 Ant 构建 Tomcat 几个月后,很明显 Java 属性文件不足以表达复杂的构建指令。文件需要签出、复制、编译、发送到另一台计算机并进行单元测试。如果失败,需要将电子邮件发送给适当的人。如果成功,需要以尽可能高的音量播放“Bad to the Bone”。在曲目结束时,音量必须恢复到原来的水平。是的,Java 属性文件不再削减它了。James需要一个更灵活的解决方案。他不想编写自己的解析器(特别是因为他想要一个行业标准的解决方案)。XML 似乎是一个合理的选择。几天后,Ant 被移植到 XML 上。这是自切片面包以来最好的东西。
So how does Ant work? It's pretty simple. It takes an XML file with specific build instructions (you decide if they're data or code) and interprets them by running specialized Java code for each XML element. It's actually much simpler than it sounds. A simple XML instruction like the one below causes a Java class with an equivalent name to be loaded and its code to be executed.
那么蚂蚁金服是如何工作的呢?这很简单。它采用一个包含特定构建指令的 XML 文件(您可以决定它们是数据还是代码),并通过为每个 XML 元素运行专用的 Java 代码来解释它们。它实际上比听起来简单得多。像下面的简单 XML 指令会导致加载具有等效名称的 Java 类并执行其代码。
<copy todir="../new/dir">
<fileset dir="src_dir"/>
</copy>
The snippet above copies a source directory to a destination directory. Ant locates a "copy" task (a Java class, really), sets appropriate parameters (todir and fileset) by calling appropriate Java methods and then executes the task. Ant comes with a set of core tasks and anyone can extend it with tasks of their own simply by writing Java classes that follow certain conventions. Ant finds these classes and executes them whenever XML elements with appropriate names are encountered. Pretty simple. Effectively Ant accomplishes what we were talking about in the previous section: it acts as an interpreter for a language that uses XML as its syntax by translating XML elements to appropriate Java instructions. We could write an "add" task and have Ant execute it when it encounters the XML snippet for addition presented in the previous section! Considering that Ant is an extremely popular project, the ideas presented in the previous section start looking more sane. After all, they're being used every day in what probably amounts to thousands of companies!
上面的代码片段将源目录复制到目标目录。Ant 找到一个“复制”任务(实际上是一个 Java 类),通过调用适当的 Java 方法设置适当的参数(todir 和 fileset),然后执行该任务。Ant 带有一组核心任务,任何人都可以通过编写遵循某些约定的 Java 类来扩展它,只需编写自己的任务即可。Ant 会找到这些类,并在遇到具有适当名称的 XML 元素时执行它们。很简单。实际上,Ant 完成了我们在上一节中讨论的内容:它通过将 XML 元素转换为适当的 Java 指令来充当使用 XML 作为其语法的语言的解释器。我们可以编写一个“添加”任务,并让 Ant 在遇到上一节中介绍的 XML 代码片段时执行它!考虑到蚂蚁是一个非常受欢迎的项目,上一节中提出的想法开始看起来更加理智。毕竟,它们每天都在数千家公司中使用!
So far I've said nothing about why Ant actually goes through all the trouble of interpreting XML. Don't try to look for the answer on its website either - you'll find nothing of value. Nothing relevant to our discussion, anyway. Let's take another step. It's time to find out why.
到目前为止,我还没有说过为什么 Ant 实际上会经历解释 XML 的所有麻烦。也不要试图在它的网站上寻找答案——你不会发现任何有价值的东西。无论如何,与我们的讨论无关。让我们再迈出一步。是时候找出原因了。
Why XML? 为什么选择 XML?
Sometimes right decisions are made without full conscious understanding of all the issues involved. I'm not sure if James knew why he chose XML - it was likely a subconscious decision. At the very least, the reasons I saw on Ant's website for using XML are all the wrong reasons. It appears that the main concerns revolved around portability and extensibility. I fail to see how XML helps advance these goals in Ant's case. What is the advantage of using interpreted XML over simple Java source code? Why not create a set of classes with a nice API for commonly used tasks (copying directories, compiling, etc.) and using those directly from Java source code? This would run on every platform that runs Java (which Ant requires anyway), it's infinitely extensible, and it has the benefit of having a more pleasant, familiar syntax. So why XML? Can we find a good reason for using it?
有时,正确的决定是在没有完全有意识地了解所有相关问题的情况下做出的。我不确定 James 是否知道他为什么选择 XML——这可能是一个潜意识的决定。至少,我在蚂蚁网站上看到的使用 XML 的原因都是错误的。似乎主要关注点围绕着可移植性和可扩展性。在蚂蚁的案例中,我看不出XML是如何帮助实现这些目标的。与简单的 Java 源代码相比,使用解释型 XML 有什么优势?为什么不为常用任务(复制目录、编译等)创建一组带有漂亮 API 的类,并直接从 Java 源代码中使用这些类呢?这将在运行 Java 的每个平台上运行(无论如何 Ant 都需要),它是无限可扩展的,并且它的好处是拥有更愉快、更熟悉的语法。那么为什么选择 XML?我们能找到使用它的充分理由吗?
It turns out that we can (although as I mentioned earlier I'm not sure if James was consciously aware of it). XML has the property of being far more flexible in terms of introduction of semantic constructs than Java could ever hope to be. Don't worry, I'm not falling into the trap of using big words to describe incomprehensible concepts. This is actually a relatively simple idea, though it may take some effort to explain. Buckle your seat-belt. We're about to make a giant leap towards achieving nirvana.
事实证明,我们可以(尽管正如我之前提到的,我不确定詹姆斯是否有意识地意识到这一点)。XML 在引入语义结构方面具有比 Java 所希望的要灵活得多的特性。别担心,我不会落入用大词来描述难以理解的概念的陷阱。这实际上是一个相对简单的想法,尽管可能需要一些努力来解释。系好安全带。我们即将朝着实现涅槃迈出一大步。
How can we represent 'copy' example above in Java code? Here's one way to do it:
我们如何在 Java 代码中表示上面的“复制”示例?这是一种方法:
CopyTask copy = new CopyTask();
Fileset fileset = new Fileset();
fileset.setDir("src_dir");
copy.setToDir("../new/dir");
copy.setFileset(fileset);
copy.execute();
The code is almost the same, albeit a little longer than the original XML. So what's different? The answer is that the XML snippet introduces a special semantic construct for copying. If we could do it in Java it would look like this:
代码几乎相同,尽管比原始 XML 长一点。那么有什么不同呢?答案是 XML 代码段引入了一种特殊的复制语义结构。如果我们可以在 Java 中做到这一点,它将看起来像这样:
copy("../new/dir")
{
fileset("src_dir");
}
Can you see the difference? The code above (if it were possible in Java) is a special operator for copying files - similar to a for loop or a new foreach construct introduced in Java 5. If we had an automatic converter from XML to Java it would likely produce the above gibberish. The reason for this is that Java's accepted syntax tree grammar is fixed by the language specification - we have no way of modifying it. We can add packages, classes, methods, but we cannot extend Java to make addition of new operators possible. Yet we can do it to our heart's content in XML - its syntax tree isn't restricted by anything except our interpreter! If the idea is still unclear, consider introducing a special operator 'unless' to Java:
你能看出区别吗?上面的代码(如果在 Java 中可能的话)是一个用于复制文件的特殊运算符 - 类似于 for 循环或 Java 5 中引入的新 foreach 构造。如果我们有一个从XML到Java的自动转换器,它可能会产生上述乱码。这样做的原因是 Java 接受的语法树语法是由语言规范固定的——我们没有办法修改它。我们可以添加包、类、方法,但我们不能扩展 Java 以添加新的运算符。然而,我们可以在XML中随心所欲地做到这一点 - 它的语法树除了我们的解释器之外不受任何限制!如果这个想法仍然不清楚,可以考虑在 Java 中引入一个特殊的运算符 'unless':
unless(someObject.canFly())
{
someObject.transportByGround();
}
In the previous two examples we extend the Java language to introduce an operator for copying files and a conditional operator unless. We would do this by modifying the abstract syntax tree grammar that Java compiler accepts. Naturally we cannot do it with standard Java facilities, but we can easily do it in XML. Because our XML interpreter parses the abstract syntax tree that results from it, we can extend it to include any operator we like.
在前两个示例中,我们扩展了 Java 语言,引入了用于复制文件的运算符和条件运算符,除非。我们将通过修改 Java 编译器接受的抽象语法树语法来做到这一点。当然,我们不能用标准的Java工具做到这一点,但我们可以很容易地用XML来做到这一点。因为我们的 XML 解释器解析了它产生的抽象语法树,我们可以扩展它以包含我们喜欢的任何运算符。
For complex operators this ability provides tremendous benefits. Can you imagine writing special operators for checking out source code, compiling files, running unit testing, sending email? Try to come up with some. If you're dealing with a specialized problem (in our case it's building projects) these operators can do wonders to decrease the amount of code you have to type and to increase clarity and code reuse. Interpreted XML makes this extremely easy to accomplish because it's a simple data file that stores hierarchical data. We do not have this option in Java because it's hierarchical structure is fixed (as you will soon find out, we do have this option in Lisp). Perhaps this is one of the reasons why Ant is so successful?
对于复杂的操作员来说,这种能力提供了巨大的好处。你能想象编写特殊的运算符来检查源代码、编译文件、运行单元测试、发送电子邮件吗?试着想出一些。如果你正在处理一个专门的问题(在我们的例子中是构建项目),这些运算符可以创造奇迹,减少你必须键入的代码量,提高清晰度和代码重用率。解释型 XML 使这非常容易实现,因为它是存储分层数据的简单数据文件。我们在 Java 中没有这个选项,因为它的层次结构是固定的(你很快就会发现,我们在 Lisp 中确实有这个选项)。也许这就是蚂蚁金服如此成功的原因之一?
I urge you to take a look at recent evolution of Java and C# (especially the recently released specification for C# 3.0). The languages are being evolved by abstracting away commonly used functionality and adding it in the form of operators. New C# operators for built-in queries is one example. This is accomplished by relatively traditional means: language creators modify the accepted abstract syntax tree and add implementations of certain features. Imagine the possibilities if the programmer could modify the abstract syntax tree himself! Whole new sub-languages could be built for specialized domains (for example a language for building projects, like Ant). Can you come up with other examples? Think about these concepts for a bit, but don't worry about them too much. We'll come back to these issues after introducing a few more ideas. By then things will be a little more clear.
我恳请您看一下 Java 和 C# 的最新发展(尤其是最近发布的 C# 3.0 规范)。这些语言正在通过抽象出常用的功能并以运算符的形式添加它来发展。用于内置查询的新 C# 运算符就是一个示例。这是通过相对传统的方式实现的:语言创建者修改公认的抽象语法树并添加某些功能的实现。想象一下,如果程序员可以自己修改抽象语法树,那会有多大的可能性!可以为专门的领域构建全新的子语言(例如,用于构建项目的语言,如 Ant)。你能想出其他例子吗?想一想这些概念,但不要太担心它们。在介绍更多想法之后,我们将回到这些问题。到那时,事情会更清楚一些。
Almost Lisp 几乎是 Lisp
Let's forget about the operator business for the moment and try to expand our horizons beyond the constraints of Ant's design. I mentioned earlier that Ant can be extended by writing conventional Java classes. Ant interpreter then attempts to match XML elements to appropriately named Java classes and if the match is found the task is executed. An interesting question begs to be asked. Why not extend Ant in Ant itself? After all, core tasks contain a lot of conventional programming language constructs ('if' being a perfect example). If Ant provided constructs to develop tasks in Ant itself we'd reach a higher degree of portability. We'd be dependent on a core set of tasks (a standard library, if you will) and we wouldn't care if Java runtime is present: the core set could be implemented in anything. The rest of the tasks would be built on top of the core using Ant-XML itself. Ant would then become a generic, extensible, XML-based programming language. Consider the possibilities:
让我们暂时忘记运营商业务,并尝试将我们的视野扩展到蚂蚁设计的限制之外。我之前提到过,Ant 可以通过编写传统的 Java 类来扩展。然后,Ant 解释器尝试将 XML 元素与适当命名的 Java 类匹配,如果找到匹配项,则执行任务。一个有趣的问题需要被问到。为什么不在 Ant 本身中扩展 Ant?毕竟,核心任务包含许多传统的编程语言结构(“if”就是一个完美的例子)。如果 Ant 提供了在 Ant 本身中开发任务的构造,我们将达到更高程度的可移植性。我们将依赖于一组核心任务(如果你愿意的话,一个标准库),我们并不关心是否存在 Java 运行时:核心集可以在任何内容中实现。其余的任务将使用Ant-XML本身构建在核心之上。然后,Ant 将成为一种通用的、可扩展的、基于 XML 的编程语言。考虑以下可能性:
<task name="Test">
<echo message="Hello World!"/>
</task>
<Test />
If ant supported the "task" construct, the example above would print "Hello World!". In fact, we could write a "task" task in Java and make Ant able to extend itself using Ant-XML! Ant would then be able to build more complicated primitives on top of simple ones, just like any other programming language! This is an example of "XML" based programming language we were talking about in the beginning of this tutorial. Not very useful (can you tell why?) but pretty damn cool.
如果 ant 支持 “task” 构造,则上面的示例将打印 “Hello World!”。事实上,我们可以用 Java 编写一个“任务”任务,让 Ant 能够使用 Ant-XML 扩展自己!然后,Ant 将能够在简单的基元之上构建更复杂的基元,就像任何其他编程语言一样!这是我们在本教程开头讨论的基于“XML”的编程语言的一个示例。不是很有用(你能说出为什么吗?)但很酷。
By the way, take a look at our 'Test' task once again. Congratulations. You're looking at Lisp code. What on Earth am I talking about? It doesn't look anything like Lisp? Don't worry, we'll fix that in a bit. Confused? Good. Let's clear it all up!
顺便说一句,再看看我们的“测试”任务。祝贺。你正在看Lisp代码。我到底在说什么?它看起来一点都不像Lisp?别担心,我们稍后会解决这个问题。困惑?好。让我们把一切都弄清楚吧!
A Better XML 更好的 XML
I mentioned in the previous section that self-extending Ant wouldn't be very useful. The reason for that is XML's verbosity. It's not too bad for data files but the moment you try writing reasonably complex code the amount of typing you have to do quickly starts to get in the way and progresses to becoming unusable for any real project. Have you ever tried writing Ant build scripts? I have, and once they get complex enough having to do it in XML becomes really annoying. Imagine having to type almost everything in Java twice because you have to close every element. Wouldn't that drive you nuts?
我在上一节中提到过,自我扩展的 Ant 不是很有用。原因是 XML 的冗长。对于数据文件来说,这还不错,但是当你尝试编写相当复杂的代码时,你必须做的大量的输入很快就会开始成为阻碍,并发展到对任何实际项目都无法使用。你有没有尝试过编写 Ant 构建脚本?我有,一旦它们变得足够复杂,就必须在XML中执行它变得非常烦人。想象一下,你必须在 Java 中输入几乎所有内容两次,因为你必须关闭每个元素。这不会让你发疯吗?
The solution to this problem involves using a less verbose alternative to XML. Remember, XML is just a format for representing hierarchical data. We don't have to use XML's angle brackets to serialize trees. We could come up with many other formats. One such format (incidentally, the one Lisp uses) is called an s-expression. S-expressions accomplish the same goals as XML. They're just a lot less verbose, which makes them much better suited for typing code. I will explain s-expressions in a little while, but before I do I have to clear up a few things about XML. Let's consider our XML example for copying files:
此问题的解决方案涉及使用不太详细的 XML 替代方法。请记住,XML 只是一种表示分层数据的格式。我们不必使用 XML 的尖括号来序列化树。我们可以想出许多其他格式。其中一种格式(顺便说一句,Lisp使用的格式)被称为s表达式。S 表达式实现与 XML 相同的目标。它们只是不那么冗长,这使得它们更适合键入代码。我稍后将解释 s 表达式,但在此之前,我必须澄清一些关于 XML 的事情。让我们考虑一下用于复制文件的 XML 示例:
<copy todir="../new/dir">
<fileset dir="src_dir"/>
</copy>
Think of what the parse tree of this snippet would look like in memory. We'd have a 'copy' node that contains a fileset node. But what about attributes? How do they fit into our picture? If you've ever used XML to describe data and wondered whether you should use an element or an attribute, you're not alone. Nobody can really figure this out and doing it right tends to be black magic rather than science. The reason for that is that attributes are really subsets of elements. Anything attributes can do, elements can do as well. The reason attributes were introduced is to curb XML's verbosity. Take a look at another version of our 'copy' snippet:
想想这个代码片段的解析树在内存中会是什么样子。我们将有一个包含文件集节点的“copy”节点。但是属性呢?它们如何融入我们的形象?如果您曾经使用过 XML 来描述数据,并且想知道是否应该使用元素或属性,那么您并不孤单。没有人能真正弄清楚这一点,正确地做到这一点往往是黑魔法而不是科学。原因是属性实际上是元素的子集。属性可以做的任何事情,元素也可以做。引入属性的原因是为了遏制 XML 的冗长性。看看我们“复制”片段的另一个版本:
<copy>
<todir>../new/dir</todir>
<fileset>
<dir>src_dir</dir>
</fileset>
</copy>
The two snippets hold exactly the same information. However, we use attributes to avoid typing the same thing more than once. Imagine if attributes weren't part of XML specification. Writing anything in XML would drive us nuts!
这两个代码段包含完全相同的信息。但是,我们使用属性来避免多次键入相同的内容。想象一下,如果属性不是 XML 规范的一部分。用XML编写任何东西都会让我们发疯!
Now that we got attributes out of the way, let's look at s-expressions. The reason we took this detour is that s-expressions do not have attributes. Because they're a lot less verbose, attributes are simply unnecessary. This is one thing we need to keep in mind when transforming XML to s-expressions. Let's take a look at an example. We could translate above snippet to s-expressions like this:
现在我们已经了解了属性,让我们看一下 s 表达式。我们之所以走这个弯路,是因为 s 表达式没有属性。因为它们不那么冗长,所以属性根本不必要。这是我们在将 XML 转换为 s 表达式时需要牢记的一件事。让我们看一个例子。我们可以将上面的片段翻译成这样的 s 表达式:
(copy
(todir "../new/dir")
(fileset (dir "src_dir")))
Take a good look at this representation. What's different? Angle brackets seem to be replaced by parentheses. Instead of enclosing each element into a pair of parentheses and then closing each element with a "(/element)" we simply skip the second parenthesis in "(element" and proceed. The element is then closed like this: ")". That's it! The translation is natural and very simple. It's also a lot easier to type. Do parentheses blind first time users? Maybe, but now that we're understand the reasoning behind them they're a lot easier to handle. At the very least they're better than arthritis inducing verbosity of XML. After you get used to s-expressions writing code in them is not only doable but very pleasant. And they provide all the benefits of writing code in XML (many of which we're yet to explore). Let's take a look at our 'task' code in something that looks a lot more like lisp:
好好看看这个表示法。有什么不同?尖括号似乎被括号取代。我们不需要将每个元素括在一对括号中,然后用“(/element)”结束每个元素,而是跳过“(element”中的第二个括号并继续)。然后,该元素将像这样关闭:“)”。就是这样!翻译很自然,非常简单。打字也容易得多。括号会让初次使用的用户盲目吗?也许吧,但现在我们已经了解了它们背后的原因,它们更容易处理。至少,它们比关节炎引起的XML冗长要好。在你习惯了 s 表达式之后,用它们编写代码不仅是可行的,而且非常愉快。它们提供了用 XML 编写代码的所有好处(其中许多我们尚未探索)。让我们看一下我们的“任务”代码,它看起来更像 lisp:
(task (name "Test")
(echo (message "Hello World!")))
(Test)
S-expressions are called lists in Lisp lingo. Consider our 'task' element above. If we rewrite it without a line break and with comas instead of spaces it's starting to look surprisingly like a list of elements and other lists (the formatting is added to make it easier to see nested lists):
S-表达式在Lisp术语中被称为列表。考虑上面的“任务”元素。如果我们在不带换行符的情况下重写它,并且用逗号而不是空格,它开始看起来令人惊讶地像元素和其他列表的列表(添加格式是为了更容易看到嵌套列表):
(task, (name, "test"), (echo, (message, "Hello World!")))
We could do the same with XML. Of course the line above isn't really a list, it's a tree, just like its XML-alternative. Don't let references to lists confuse you, it's just that lists that contain other lists and trees are effectively the same thing. Lisp may stand for List Processing, but it's really tree processing - no different than processing XML nodes.
我们可以对 XML 做同样的事情。当然,上面的那一行并不是一个真正的列表,它是一棵树,就像它的XML替代品一样。不要让对列表的引用让您感到困惑,只是包含其他列表和树的列表实际上是一回事。Lisp可能代表列表处理,但它实际上是树处理 - 与处理XML节点没有什么不同。
Whew. After much rambling we finally got to something that looks like Lisp (and is Lisp, really). By now the mysterious Lisp parentheses as well as some claims made by Lisp advocates should become more clear. But we still have a lot of ground to cover. Ready? Let's move on!
呼。经过一番漫无边际的思考,我们终于找到了一个看起来像 Lisp 的东西(而且是 Lisp,真的)。到现在为止,神秘的Lisp括号以及Lisp倡导者提出的一些主张应该变得更加清晰。但我们还有很多工作要做。准备?让我们继续前进!
C Macros Reloaded 重新加载 C 宏
By now you must be tired of all the XML talk. I'm tired of it as well. It's time to take a break from all the trees, s-expressions, and Ant business. Instead, let's go back to every programmer's roots. It's time to talk about C preprocessor. What's C got to do with anything, I hear you ask? Well, we now know enough to get into metaprogramming and discuss code that writes other code. Understanding this tends to be hard since all tutorials discuss it in terms of languages that you don't know. But there is nothing hard about the concept. I believe that a metaprogramming discussion based on C will make the whole thing much easier to understand. So, let's see (pun intended).
到现在为止,您一定已经厌倦了所有的 XML 讨论。我也厌倦了。是时候从所有的树、s 表达式和 Ant 业务中休息一下了。相反,让我们回到每个程序员的根源。是时候谈谈 C 预处理器了。我听到你问,C 和任何事情有什么关系?好了,我们现在知道的已经足够多了,可以进入元编程并讨论编写其他代码的代码。理解这一点往往很困难,因为所有教程都用你不知道的语言来讨论它。但这个概念并不难。我相信基于 C 语言的元编程讨论将使整个事情更容易理解。所以,让我们看看(双关语)。
Why would anyone want to write a program that writes programs? How can we use something like this in the real world? What on Earth is metaprogramming, anyway? You already know all the answers, you just don't know it yet. In order to unlock the hidden vault of divine knowledge let's consider a rather mundane task of simple database access from code. We've all been there. Writing SQL queries all over the code to modify data within tables turns into repetitive hell soon enough. Even with the new C# 3.0 LINQ stuff this is a huge pain. Writing a full SQL query (albeit with a nice built in syntax) to get someone's name or to modify someone's address isn't exactly a programmer's idea of comfort. What do we do to solve these problems? Enter data access layers.
为什么有人想写一个写程序的程序?我们如何在现实世界中使用这样的东西?到底什么是元编程?你已经知道所有的答案,只是你还不知道。为了解开隐藏的神圣知识宝库,让我们考虑一个相当平凡的任务,即从代码中访问数据库。我们都去过那里。在代码中编写 SQL 查询以修改表中的数据很快就会变成重复的地狱。即使使用新的 C# 3.0 LINQ 内容,这也是一个巨大的痛苦。编写一个完整的 SQL 查询(尽管有一个不错的内置语法)来获取某人的名字或修改某人的地址并不完全是程序员的舒适想法。我们该怎么做才能解决这些问题?输入数据访问层。
The idea is simple enough. You abstract database access (at least trivial queries, anyway) by creating a set of classes that mirror the tables in the database and use accessor methods to execute actual queries. This simplifies development tremendously - instead of writing SQL queries we make simple method calls (or property assignments, depending on your language of choice). Anyone who has ever used even the simplest of data access layers knows how much time it can save. Of course anyone who has ever written one knows how much time it can kill - writing a set of classes that mirror tables and convert accessors to SQL queries takes a considerable chunk of time. This seems especially silly since most of the work is manual: once you figure out the design and develop a template for your typical data access class you don't need to do any thinking. You just write code based on the same template over and over and over and over again. Many people figured out that there is a better way - there are plenty of tools that connect to the database, grab the schema, and write code for you based on a predefined (or a custom) template.
这个想法很简单。通过创建一组类来抽象数据库访问(至少是琐碎的查询),这些类镜像数据库中的表并使用访问器方法来执行实际查询。这极大地简化了开发 - 我们不是编写 SQL 查询,而是进行简单的方法调用(或属性分配,具体取决于您选择的语言)。任何曾经使用过最简单的数据访问层的人都知道它可以节省多少时间。当然,任何曾经编写过它的人都知道它会浪费多少时间——编写一组镜像表并将访问器转换为 SQL 查询的类需要花费大量时间。这似乎特别愚蠢,因为大部分工作都是手动完成的:一旦你弄清楚了设计,并为典型的数据访问类开发了一个模板,你就不需要做任何思考。您只需一遍又一遍地基于相同的模板编写代码。许多人发现有一种更好的方法 - 有很多工具可以连接到数据库,获取架构,并根据预定义(或自定义)模板为您编写代码。
Anyone who has ever used such a tool knows what an amazing time saver it can be. In a few clicks you connect the tool to the database, get it to generate the data access layer source code, add the files to your project and voilà - ten minutes worth of work do a better job than hundreds of man-hours that were required previously. What happens if your database schema changes? Well, you just have to go through this short process again. Of course some of the best tools let you automate this - you simply add them as a part of your build step and every time you compile your project everything is done for you automatically. This is perfect! You barely have to do anything at all. If the schema ever changes your data access layer code updates automatically at compile time and any obsolete access in your code will result in compiler errors!
任何曾经使用过这种工具的人都知道它可以节省多少时间。只需单击几下,即可将工具连接到数据库,让它生成数据访问层源代码,将文件添加到您的项目中,瞧 - 十分钟的工作比以前所需的数百个工时做得更好。如果数据库架构发生更改,会发生什么情况?好吧,你只需要再经历一次这个简短的过程。当然,一些最好的工具可以让你自动化这一点 - 你只需将它们添加为构建步骤的一部分,每次编译项目时,一切都会自动为你完成。这太完美了!你几乎不需要做任何事情。如果架构发生更改,则数据访问层代码会在编译时自动更新,并且代码中的任何过时访问都会导致编译器错误!
Data access layers are one good example, but there are plenty of others. From boilerplate GUI code, to web code, to COM and CORBA stubs, to MFC and ATL, - there are plenty of examples where the same code is written over and over again. Since writing this code is a task that can be automated completely and a programmer's time is far more expensive than CPU time, plenty of tools have been created that generate this boilerplate code automatically. What are these tools, exactly? Well, they are programs that write programs. They perform a simple task that has a mysterious name of metaprogramming. That's all there is to it.
数据访问层就是一个很好的例子,但还有很多其他的例子。从样板 GUI 代码到 Web 代码,再到 COM 和 CORBA 存根,再到 MFC 和 ATL,还有很多示例可以一遍又一遍地编写相同的代码。由于编写此代码是一项可以完全自动化的任务,并且程序员的时间比 CPU 时间昂贵得多,因此已经创建了许多工具来自动生成此样板代码。这些工具到底是什么?嗯,它们是编写程序的程序。他们执行一个简单的任务,该任务有一个神秘的名称,即元编程。这就是它的全部内容。
We could create and use such tools in millions of scenarios but more often than not we don't. What it boils down to is a subconscious calculation - is it worth it for me to create a separate project, write a whole tool to generate something, and then use it, if I only have to write these very similar pieces about seven times? Of course not. Data access layers and COM stubs are written hundreds, thousands of times. This is why there are tools for them. For similar pieces of code that repeat only a few times, or even a few dozen times, writing code generation tools isn't even considered. The trouble to create such a tool more often than not far outweighs the benefit of using one. If only creating such tools was much easier, we could use them more often, and perhaps save many hours of our time. Let's see if we can accomplish this in a reasonable manner.
我们可以在数以百万计的场景中创建和使用这样的工具,但通常情况下我们做不到。归根结底,这是一个潜意识的计算——如果我只需要写这些非常相似的文章大约七次,那么我是否应该创建一个单独的项目,编写一个完整的工具来生成一些东西,然后使用它?当然不是。数据访问层和 COM 存根被写入数百次、数千次。这就是为什么有适合他们的工具。对于只重复几次甚至几十次的类似代码,甚至不考虑编写代码生成工具。创建这种工具的麻烦往往远远超过使用工具的好处。如果创建这样的工具要容易得多,我们就可以更频繁地使用它们,也许可以节省很多时间。让我们看看我们是否能以合理的方式做到这一点。
Surprisingly C preprocessor comes to the rescue. We've all used it in C and C++. On occasion we all wish Java had it. We use it to execute simple instructions at compile time to make small changes to our code (like selectively removing debug statements). Let's look at a quick example:
令人惊讶的是,C 预处理器派上用场了。我们都在 C 和 C++ 中使用过它。有时我们都希望 Java 拥有它。我们用它来在编译时执行简单的指令,对我们的代码进行一些小的更改(比如有选择地删除调试语句)。让我们看一个简单的例子:
#define triple(X) X + X + X
What does this line do? It's a simple instruction written in the preprocessor language that instructs it to replace all instances of triple(X) with X + X + X. For example all instances of 'triple(5)' will be replaced with '5 + 5 + 5' and the resulting code will be compiled by the C compiler. We're really doing a very primitive version of code generation here. If only C preprocessor was a little more powerful and included ways to connect to the database and a few more simple constructs, we could use it to develop our data access layer right there, from within our program! Consider the following example that uses an imaginary extension of the C preprocessor:
这条线是做什么的?这是用预处理器语言编写的简单指令,指示它用 X + X + X 替换 triple(X) 的所有实例。例如,“triple(5)”的所有实例都将替换为“5 + 5 + 5”,生成的代码将由 C 编译器编译。我们在这里做的是代码生成的一个非常原始的版本。如果只有 C 预处理器功能更强大一点,并且包含连接到数据库的方法和一些更简单的结构,我们就可以使用它从我们的程序中开发我们的数据访问层!请考虑以下示例,该示例使用 C 预处理器的假想扩展:
#get-db-schema("127.0.0.1, un, pwd");
#iterate-through-tables
#for-each-table
class #table-name
{
};
#end-for-each
We've just connected to the database schema, iterated through all the tables, and created an empty class for each. All in a couple of lines right within our source code! Now every time we recompile the file where above code appears we'll get a freshly built set of classes that automatically update based on the schema. With a little imagination you can see how we could build a full data access layer straight from within our program, without the use of any external tools! Of course this has a certain disadvantage (aside from the fact that such an advanced version of C preprocessor doesn't exist) - we'd have to learn a whole new "compile-time language" to do this sort of work. For complex code generation this language would have to be very complex as well, it would have to support many libraries and language constructs. For example, if our generated code depended on some file located at some ftp server the preprocessor would have to be able to connect to ftp. It's a shame to create and learn a new language just to do this. Especially since there are so many nice languages already out there. Of course if we add a little creativity we can easily avoid this pitfall.
我们刚刚连接到数据库架构,遍历了所有表,并为每个表创建了一个空类。所有这些都在我们的源代码中!现在,每次我们重新编译出现上述代码的文件时,我们都会得到一组新构建的类,这些类会根据架构自动更新。只要有一点想象力,你就可以看到我们如何直接从我们的程序中构建一个完整的数据访问层,而无需使用任何外部工具!当然,这有一定的缺点(除了不存在这种高级版本的 C 预处理器之外)——我们必须学习一种全新的“编译时语言”才能完成此类工作。对于复杂的代码生成,这种语言也必须非常复杂,它必须支持许多库和语言结构。例如,如果我们生成的代码依赖于位于某个 ftp 服务器上的某个文件,则预处理器必须能够连接到 ftp。仅仅为了做到这一点而创造和学习一门新语言是一种耻辱。特别是因为那里已经有很多好的语言了。当然,如果我们增加一点创造力,我们可以很容易地避免这个陷阱。
Why not replace the preprocessor language with C/C++ itself? We'd have full power of the language at compile time and we'd only need to learn a few simple directives to differentiate between compile time and runtime code!
为什么不用 C/C++ 本身替换预处理器语言呢?在编译时,我们将拥有该语言的全部功能,我们只需要学习一些简单的指令来区分编译时和运行时代码!
<%
cout << "Enter a number: ";
cin >> n;
%>
for(int i = 0; i < <%= n %>; i++)
{
cout << "hello" << endl;
}
Can you see what happens here? Everything that's between <% and %> tags runs when the program is compiled. Anything outside of these tags is normal code. In the example above you'd start compiling your program in the development environment. The code between the tags would be compiled and then ran. You'd get a prompt to enter a number. You'd enter one and it would be placed inside the for loop. The for loop would then be compiled as usual and you'd be able to execute it. For example, if you'd enter 5 during the compilation of your program, the resulting code would look like this:
你能看到这里发生了什么吗?在编译程序时,<% 和 %> 标记之间的所有标记都会运行。这些标记之外的任何内容都是普通代码。在上面的示例中,您将开始在开发环境中编译程序。标记之间的代码将被编译,然后运行。您会收到输入数字的提示。你输入一个,它就会被放在for循环中。然后,for 循环将像往常一样编译,您将能够执行它。例如,如果在编译程序期间输入 5,则生成的代码将如下所示:
for(int i = 0; i < 5; i++)
{
cout << "hello" << endl;
}
Simple and effective. No need for a special preprocessor language. We get full power of our host language (in this case C/C++) at compile time. We could easily connect to a database and generate our data access layer source code at compile time in the same way JSP or ASP generate HTML! Creating such tools would also be tremendously quick and simple. We'd never have to create new projects with specialized GUIs. We could inline our tools right into our programs. We wouldn't have to worry about whether writing such tools is worth it because writing them would be so fast - we could save tremendous amounts of time by creating simple bits of code that do mundane code generation for us!
简单而有效。无需特殊的预处理器语言。我们在编译时获得了宿主语言(在本例中为 C/C++)的全部功能。我们可以很容易地连接到数据库,并在编译时生成我们的数据访问层源代码,就像 JSP 或 ASP 生成 HTML 一样!创建此类工具也将非常快速和简单。我们永远不必使用专门的 GUI 创建新项目。我们可以将我们的工具直接内联到我们的程序中。我们不必担心编写这样的工具是否值得,因为编写它们的速度会非常快——我们可以通过创建简单的代码来为我们生成平凡的代码来节省大量时间!
Hello, Lisp! 你好,Lisp!
Everything we've learned about Lisp so far can be summarized by a single statement: Lisp is executable XML with a friendlier syntax. We haven't said a single word about how Lisp actually operates. It's time to fill this gap2.
到目前为止,我们对Lisp的了解都可以用一句话来概括:Lisp是可执行的XML,具有更友好的语法。我们还没有说过Lisp的实际运作方式。是时候填补这个空白 2 了。
Lisp has a number of built in data types. Integers and strings, for example, aren't much different from what you're used to. The meaning of 71 or "hello" is roughly the same in Lisp as in C++ or Java. What is of more interest to us are symbols, lists, and functions. I will spend the rest of this section describing these data types as well as how a Lisp environment compiles and executes the source code you type into it (this is called evaluation in Lisp lingo). Getting through this section in one piece is important for understanding true potential of Lisp's metaprogramming, the unity of code and data, and the notion of domain specific languages. Don't think of this section as a chore though, I'll try to make it fun and accessible. Hopefully you can pick up a few interesting ideas on the way. Ok. Let's start with Lisp's symbols.
Lisp 有许多内置的数据类型。例如,整数和字符串与您习惯的没有太大区别。71 或“hello”在 Lisp 中的含义与 C++ 或 Java 中的含义大致相同。我们更感兴趣的是符号、列表和函数。在本节的其余部分,我将描述这些数据类型,以及Lisp环境如何编译和执行你在其中输入的源代码(这在Lisp术语中称为评估)。完整地完成这一部分对于理解Lisp元编程的真正潜力、代码和数据的统一以及领域特定语言的概念非常重要。不过,不要把这部分看作是一件苦差事,我会尽量让它变得有趣和容易理解。希望您能在此过程中获得一些有趣的想法。还行。让我们从Lisp的符号开始。
A symbol in Lisp is roughly equivalent to C++ or Java's notion of an identifier. It's a name you can use to access a variable (like currentTime, arrayCount, n, etc.) The difference is that a symbol in Lisp is a lot more liberal than its mainstream identifier alternative. In C++ or Java you're limited to alphanumeric characters and an underscore. In Lisp, you are not. For example + is a valid symbol. So is -, =, hello-world, hello+world, **, etc. (you can find the exact definition of valid Lisp symbols online). You can assign to these symbols any data-type you like. Let's ignore Lisp syntax and use pseudo-code for now. Assume that a function set assigns some value to a symbol (like = does in Java or C++). The following are all valid examples:
Lisp中的符号大致相当于C++或Java的标识符概念。它是可用于访问变量的名称(如 currentTime、arrayCount、n 等)不同之处在于,Lisp 中的符号比其主流标识符替代方案要自由得多。在 C++ 或 Java 中,您只能使用字母数字字符和下划线。在 Lisp 中,你不是。例如,+ 是有效符号。-、=、hello-world、hello+world、 等也是如此(你可以在网上找到有效 Lisp 符号的确切定义)。您可以为这些符号指定您喜欢的任何数据类型。让我们忽略 Lisp 语法,暂时使用伪代码。假设函数集为符号分配了一些值(就像 Java 或 C++ 中的 = 一样)。以下是所有有效示例:
set(test, 5) // symbol 'test' will equal an integer 5
set(=, 5) // symbol '=' will equal an integer 5
set(test, "hello") // symbol 'test' will equal a string "hello"
set(test, =) // at this point symbol '=' is equal to 5
// therefore symbol 'test' will equal to 5
set(*, "hello") // symbol '*' will equal a string "hello"
At this point something must smell wrong. If we can assign strings and integers to symbols like , how does Lisp do multiplication? After all, means multiply, right? The answer is pretty simple. Functions in Lisp aren't special. There is a data-type, function, just like integer and string, that you assign to symbols. A multiplication function is built into Lisp and is assigned to a symbol . You can reassign a different value to and you'd lose the multiplication function. Or you can store the value of the function in some other variable. Again, using pseudo-code:
在这一点上,一定有什么不对劲的地方。如果我们可以将字符串和整数分配给像 这样的符号,那么 Lisp 是如何进行乘法的呢?毕竟, 的意思是乘法,对吧?答案很简单。Lisp中的函数并不特别。有一种数据类型,函数,就像整数和字符串一样,您可以将其分配给符号。乘法函数内置于 Lisp 中,并被分配给一个符号 。您可以将不同的值重新分配给 ,否则您将丢失乘法函数。或者,您可以将函数的值存储在其他变量中。同样,使用伪代码:
*(3, 4) // multiplies 3 by 4, resulting in 12
set(temp, *) // symbol '*' is equal to the multiply function
// so temp will equal to the multiply function
set(*, 3) // sets symbol '*' to equal to 3
*(3, 4) // error, symbol '*' no longer equals to a function
// it's equal to 3
temp(3, 4) // temp equals to a multiply function
// so Lisp multiplies 3 by 4 resulting in 12
set(*, temp) // symbol '*' equals multiply function again
*(3, 4) // multiplies 3 by 4, resulting in 12
You can even do wacky stuff like reassigning plus to minus:
你甚至可以做一些古怪的事情,比如将加号重新分配到减号:
set(+, -) // the value of '-' is a built in minus function
// so now symbol '+' equals to a minus function
+(5, 4) // since symbol '+' is equal to the minus function
// this results in 1
I've used functions quite liberally in these examples but I didn't describe them yet. A function in Lisp is just a data-type like an integer, a string, or a symbol. A function doesn't have a notion of a name like in Java or C++. Instead, it stands on its own. Effectively it is a pointer to a block of code along with some information (like a number of parameters it accepts). You only give the function a name by assigning it to a symbol, just like you assign an integer or a string. You can create a function by using a built in function for creating functions, assigned to a symbol 'fn'. Using pseudo-code:
在这些示例中,我已经非常自由地使用了函数,但我还没有描述它们。Lisp 中的函数只是一种数据类型,如整数、字符串或符号。函数没有像 Java 或 C++ 那样的名称概念。相反,它独立存在。实际上,它是指向代码块的指针以及一些信息(例如它接受的许多参数)。您只能通过将函数分配给符号来为函数命名,就像您分配整数或字符串一样。您可以使用用于创建函数的内置函数来创建函数,该函数分配给符号“fn”。使用伪代码:
fn [a]
{
return *(a, 2);
}
This returns a function that takes a single parameter named 'a' and doubles it. Note that the function has no name but you can assign it to a symbol:
这将返回一个函数,该函数采用名为“a”的单个参数并将其加倍。请注意,该函数没有名称,但您可以将其分配给一个符号:
set(times-two, fn [a] { return *(a, 2); })
We can now call this function:
我们现在可以调用这个函数:
times-two(5) // returns 10
Now that we went over symbols and functions, what about lists? Well, you already know a lot about them. Lists are simply pieces of XML written in s-expression form. A list is specified by parentheses and contains Lisp data-types (including other lists) separated by a space. For example (this is real Lisp, note that we use semicolons for comments now):
现在我们已经了解了符号和函数,那么列表呢?好吧,你已经对他们了解很多了。列表只是以 s 表达式形式编写的 XML 片段。列表由括号指定,包含用空格分隔的 Lisp 数据类型(包括其他列表)。例如(这是真正的Lisp,请注意,我们现在使用分号作为注释):
() ; an empty list
(1) ; a list with a single element, 1
(1 "test") ; a list with two elements
; an integer 1 and a string "test"
(test "hello") ; a list with two elements
; a symbol test and a string "hello"
(test (1 2) "hello") ; a list with three elements, a symbol test
; a list of two integers 1 and 2
; and a string "hello"
When a Lisp system encounters lists in the source code it acts exactly like Ant does when it encounters XML - it attempts to execute them. In fact, Lisp source code is only specified using lists, just like Ant source code is only specified using XML. Lisp executes lists in the following manner. The first element of the list is treated as the name of a function. The rest of the elements are treated as functions parameters. If one of the parameters is another list it is executed using the same principles and the result is passed as a parameter to the original function. That's it. We can write real code now:
当 Lisp 系统在源代码中遇到列表时,它的行为就像 Ant 遇到 XML 时一样——它会尝试执行它们。事实上,Lisp 源代码只使用列表来指定,就像 Ant 源代码只用 XML 来指定一样。Lisp 按以下方式执行列表。列表的第一个元素被视为函数的名称。其余元素被视为函数参数。如果其中一个参数是另一个列表,则使用相同的原则执行该参数,并将结果作为参数传递给原始函数。就是这样。我们现在可以编写真正的代码:
(* 3 4) ; equivalent to pseudo-code *(3, 4).
; Symbol '*' is a function
; 3 and 4 are its parameters.
; Returns 12.
(times-two 5) ; returns 10
(3 4) ; error: 3 is not a function
(times-two) ; error, times-two expects one parameter
(times-two 3 4) ; error, times-two expects one parameter
(set + -) ; sets symbol '+' to be equal to whatever symbol '-'
; equals to, which is a minus function
(+ 5 4) ; returns 1 since symbol '+' is now equal
; to the minus function
(* 3 (* 2 2)) ; multiplies 3 by the second parameter
; (which is a function call that returns 4).
; Returns 12.
Note that so far every list we've specified was treated by a Lisp system as code. But how can we treat a list as data? Again, imagine an Ant task that accepts XML as one of its parameters. In Lisp we do this using a quote operator ' like so:
请注意,到目前为止,我们指定的每个列表都被 Lisp 系统视为代码。但是,我们如何才能将列表视为数据呢?同样,假设一个 Ant 任务接受 XML 作为其参数之一。在 Lisp 中,我们使用引号运算符来做到这一点,如下所示:
(set test '(1 2)) ; test is equal to a list of two integers, 1 and 2
(set test (1 2)) ; error, 1 is not a function
(set test '(* 3 4)) ; sets test to a list of three elements,
; a symbol *, an integer 3, and an integer 4
We can use a built in function head to return the first element of the list, and a built in function tail to return the rest of the list's elements:
我们可以使用内置函数头返回列表的第一个元素,并使用内置函数尾返回列表的其余元素:
(head '(* 3 4)) ; returns a symbol '*'
(tail '(* 3 4)) ; returns a list (3 4)
(head (tail '( * 3 4))) ; (tail '(* 3 4)) returns a list (3 4)
; and (head '(3 4)) returns 3.
(head test) ; test was set to a list in previous example
; returns a symbol '*'
You can think of built in Lisp functions as you think of Ant tasks. The difference is that we don't have to extend Lisp in another language (although we can), we can extend it in Lisp itself as we did with the times-two example. Lisp comes with a very compact set of built in functions - the necessary minimum. The rest of the language is implemented as a standard library in Lisp itself.
你可以像考虑 Ant 任务一样考虑内置的 Lisp 函数。不同之处在于,我们不必用另一种语言扩展 Lisp(尽管我们可以),我们可以用 Lisp 本身扩展它,就像我们在 times-two 示例中所做的那样。Lisp 带有一组非常紧凑的内置函数 - 这是必要的最低限度。该语言的其余部分在Lisp本身中作为标准库实现。
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 节点。然后,可以自动将节点序列化为字符串。此外,我们的工具将能够通过加载现有的 Ant 构建脚本并直接处理 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.
当然,我们可以编写允许处理 XML 转换的 Ant 任务,并在 Ant 中编写我们的生成工具。或者我们可以直接使用 Lisp。正如我们之前所看到的,列表是 Lisp 内置的数据结构,Lisp 有许多工具可以快速有效地处理列表(头和尾是最简单的)。此外,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中的元编程是使用一种称为“宏”的结构完成的。让我们尝试开发一组宏,将诸如待办事项列表之类的数据转换为处理待办事项列表的语言。
Let's recall our to-do list example. The XML looks like this:
让我们回顾一下我们的待办事项列表示例。XML 如下所示:
<todo name="housework">
<item priority="high">Clean the house.</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-expression 版本如下所示:
(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?
假设我们正在编写一个待办事项管理器应用程序。我们将待办事项序列化在一组文件中,当程序启动时,我们希望读取它们并将它们显示给用户。我们将如何使用XML和其他语言(例如Java)来做到这一点?我们会使用一些 XML 解析器来解析带有待办事项列表的 XML 文件,编写遍历 XML 树的代码并将其转换为 Java 数据结构(因为坦率地说,在 Java 中处理 DOM 是一件很痛苦的事情),然后使用这个数据结构来显示数据。现在,我们将如何在 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.
如果我们采用相同的方法,我们将使用负责解析 XML 的 Lisp 库来解析文件。然后,XML 将作为 Lisp 列表(一个 s 表达式)呈现给我们,我们将遍历列表并向用户呈现相关数据。当然,如果我们使用 Lisp,那么直接将数据作为 s 表达式持久化是有意义的,因为没有理由进行 XML 转换。我们不需要特殊的解析库,因为作为一组 s 表达式持久化的数据是有效的 Lisp,我们可以使用 Lisp 编译器来解析它并将其作为 Lisp 列表存储在内存中。请注意,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:
但我们可以做得更好。与其编写代码来遍历存储数据的 s 表达式,不如编写一个宏,允许我们将数据视为代码!宏是如何工作的?很简单,真的。回想一下,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.
其中每个参数都是一个有效的 Lisp 表达式,该表达式经过计算并传递给函数。例如,如果我们用 (+ 4 5) 替换上面的 arg1,它将被计算并将 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),而是以列表的形式传递给宏。然后,宏可以自由地用它做它喜欢的事情,包括评估它。宏的返回值是一个被视为代码的 Lisp 列表。带有宏的原始位置将替换为此代码。例如,我们可以定义一个宏加号,它接受两个参数并放入添加它们的代码。
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.
它与元编程和我们的待办事项列表问题有什么关系?首先,宏是使用列表抽象生成代码的小代码。此外,我们可以创建名为 to-do 和 item 的宏,将我们的数据替换为我们喜欢的任何代码,例如向用户显示项目的代码。
What benefits does this approach offer? We don't have to walk the list. The compiler will do it for us and will invoke appropriate macros. All we need to do is create the macros that convert our data to appropriate code!
这种方法有什么好处?我们不必走在名单上。编译器将为我们执行此操作,并调用适当的宏。我们需要做的就是创建宏,将我们的数据转换为适当的代码!
For example, a macro similar to our triple C macro we showed earlier looks like this:
例如,类似于我们之前展示的 triple C 宏的宏如下所示:
(defmacro triple (x)
'(+ ~x ~x ~x))
The quote prevents evaluation while the tilde allows it. Now every time triple is encountered in lisp code:
引号阻止评估,而波浪号允许。现在,每次在 lisp 代码中遇到 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:
我们可以为我们的待办事项列表项创建宏,这些宏将由 lisp 编译器调用,并将待办事项列表转换为代码。现在,我们的待办事项列表将被视为代码并执行。假设我们要做的就是将其打印到标准输出中供用户阅读:
(defmacro item (priority note)
'(block
(print stdout tab "Priority: "
~(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 中的待办事项列表。此类语言非常特定于特定问题域,通常称为域特定语言或 DSL。
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。我们可以创建特定领域的语言,用于创建 Web 应用程序、编写大型多人游戏、进行固定收益交易、解决蛋白质折叠问题、处理交易等。我们可以将这些语言叠加在一起,并利用我们的 Web 应用程序语言和债券交易语言创建一种用于编写基于 Web 的交易应用程序的语言。我们每天都会从这种方法中获益,就像我们从蚂蚁中获益一样。
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解析器的限制。想想使用一些支持库在 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 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的人都说是的。当然,选择权仍由您决定。
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 留言。我很高兴听到您的反馈。
1I have never met James, nor does he know about my existence. The story is entirely fictional and is based on a few postings about Ant's history I found on the internet.
1 我从未见过詹姆斯,他也不知道我的存在。这个故事完全是虚构的,是基于我在互联网上找到的一些关于蚂蚁历史的帖子。
2Lisp has many different dialects (the most popular of which are Common Lisp and Scheme). Each dialect deals with intricate details differently yet shares the same set of basic principles. Since the goal of this article is to give you an understanding of Lisp's principles I will use Blaise for examples (which at the time of this writing is vaporware). With some minor modifications these examples can be translated to other Lisp dialects.
2 Lisp有许多不同的方言(其中最流行的是Common Lisp和Scheme)。每种方言以不同的方式处理错综复杂的细节,但共享相同的基本原则。由于本文的目的是让你了解 Lisp 的原理,我将使用 Blaise 作为示例(在撰写本文时,它是 vaporware)。通过一些小的修改,这些例子可以翻译成其他Lisp方言。
发表评论