Lisp的本质(The Nature of Lisp)

 Lisp的本质(The Nature of Lisp)

                             做者 Slava Akhmechet
                             译者 Alec Jang

              出处: http://www.defmacro.org/ramblings/lisp.html


简介

最初在web的某些角落偶然看到有人赞美Lisp时, 我那时已是一个很有经验的程序员。
在个人履历上, 掌握的语言范围至关普遍, 象C++, Java, C#主流语言等等都不在话下, 
我以为我差很少知道全部的有关编程语言的事情。对待编程语言的问题上, 我以为本身不
太会遇到什么大问题。其实我大错特错了。

我试着学了一下Lisp, 结果立刻就撞了墙。我被那些范例代码吓坏了。我想不少初次接触
Lisp语言的人, 必定也有过相似的感觉。Lisp的语法太次了。一个语言的发明人, 竟然不
肯用心弄出一套漂亮的语法, 那谁还会愿意学它。反正, 我是确确实实被那些难看的无数
的括号搞蒙了。

回过神来以后, 我和Lisp社区的那伙人交谈, 诉说个人沮丧心情。结果, 立马就有一大套
理论砸过来, 这套理论在Lisp社区到处可见, 几成惯例。好比说: Lisp的括号只是表面现
象; Lisp的代码和数据的表达方式没有差异, 并且比XML语法高明许多, 因此有无穷的好
处; Lisp有强大无比的元语言能力, 程序员能够写出自我维护的代码; Lisp能够创造出针
对特定应用的语言子集; Lisp的运行时和编译时没有明确的分界; 等等, 等等, 等等。这
么长的赞美词虽然看起来至关动人, 不过对我毫无心义。没人能给我演示这些东西是如何
应用的, 由于这些东西通常来讲只有在大型系统才会用到。我争辩说, 这些东西传统语言
同样办获得。在和别人争论了数个小时以后, 我最终仍是放弃了学Lisp的念头。为何要
花费几个月的时间学习语法这么难看的语言呢? 这种语言的概念这么晦涩, 又没什么好懂
的例子。也许这语言不是该我这样的人学的。

几个月来, 我承受着这些Lisp辩护士对我心灵的重压。我一度陷入了困惑。我认识一些绝
顶聪明的人, 我对他们至关尊敬, 我看到他们对Lisp的赞美达到了宗教般的高度。这就是
说, Lisp中必定有某种神秘的东西存在, 我不能忍受本身对此的无知, 好奇心和求知欲最
终不可遏制。我因而咬紧牙关埋头学习Lisp, 通过几个月的时间费劲心力的练习, 终于,
我看到了那无穷无尽的泉水的源头。在通过脱胎换骨的磨练以后, 在通过七重地狱的煎熬
以后, 终于, 我明白了。

顿悟在忽然之间来临。曾经许屡次, 我听到别人引用雷蒙德(译者注: 论文<<大教堂和市
集>>的做者, 著名的黑客社区理论家)的话: "Lisp语言值得学习。当你学会Lisp以后, 你
会拥有深入的体验。就算你日常并不用Lisp编程, 它也会使你成为更加优秀的程序员"。
过去, 我根本不懂这些话的含义, 我也不相信这是真的。但是如今我懂得了。这些话蕴含
的真理远远超过我过去的想像。我心里体会到一种神圣的情感, 一瞬间的顿悟, 几乎使我
对电脑科学的观念发生了根本的改变。

顿悟的那一刻, 我成了Lisp的崇拜者。我体验到了宗教大师的感觉: 必定要把个人知识传
布开来, 至少要让10个迷失的灵魂获得拯救。按照一般的办法, 我把这些道理(就是刚开
始别人砸过来的那一套, 不过如今我明白了真实的含义)告诉旁人。结果太使人失望了, 
只有少数几我的在我坚持之下, 发生了一点兴趣, 可是仅仅看了几眼Lisp代码, 他们就退
却了。照这样的办法, 也许费数年功夫能造就了几个Lisp迷, 但我以为这样的结果太差强
人意了, 我得想一套有更好的办法。

我深刻地思考了这个问题。是否是Lisp有什么很艰深的东西, 令得那么多老练的程序员都
不能领会? 不是, 没有任何绝对艰深的东西。由于我能弄懂, 我相信其余人也必定能。那
么问题出在那里? 后来我终于找到了答案。个人结论就是, 凡是教人学高级概念, 必定要
从他已经懂得的东西开始。若是学习过程颇有趣, 学习的内容表达得很恰当, 新概念就会
变得至关直观。这就是个人答案。所谓元编程, 所谓数据和代码形式合一, 所谓自修改代
码, 所谓特定应用的子语言, 全部这些概念根本就是同族概念, 彼此互为解释, 确定越讲
越不明白。仍是从实际的例子出发最有用。

我把个人想法说给Lisp程序员听, 遭到了他们的反对。"这些东西自己固然不可能用熟悉
的知识来解释, 这些概念彻底不同凡响, 你不可能在别人已有的经验里找到相似的东西",
但是我认为这些都是遁词。他们又反问我, "你本身为啥不试一下?" 好吧, 我来试一下。
这篇文章就是我尝试的结果。我要用熟悉的直观的方法来解释Lisp, 我但愿有勇气的人读
完它, 拿杯饮料, 深呼吸一下, 准备被搞得晕头转向。来吧, 愿你得到大能。

从新审视XML

千里之行始于足下。让咱们的第一步从XML开始。但是XML已经说得更多的了, 还能有什么
新意思可说呢? 有的。XML自身虽然谈谈不上有趣, 可是XML和Lisp的关系却至关有趣。
XML和Lisp的概念有着惊人的类似之处。XML是咱们通向理解Lisp的桥梁。好吧, 咱们且把
XML看成活马医。让咱们拿好手杖, 对XML的无人涉及的荒原地带做一番探险。咱们要从一
个全新的视角来考察这个题目。

表面上看, XML是一种标准化语法, 它以适合人阅读的格式来表达任意的层次化数据
(hirearchical data)。象任务表(to-do list), 网页, 病历, 汽车保险单, 配置文件等
等, 都是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>

解析这段数据时会发生什么状况? 解析以后的数据在内存中怎样表示? 显然, 用树来表示
这种层次化数据是很恰当的。说到底, XML这种比较容易阅读的数据格式, 就是树型结构
数据通过序列化以后的结果。任何能够用树来表示的数据, 一样能够用XML来表示, 反之
亦然。但愿你能懂得这一点, 这对下面的内容极其重要。

再进一步。还有什么类型的数据也经常使用树来表示? 无疑列表(list)也是一种。上过编译课
吧? 还模模糊糊记得一点吧? 源代码在解析以后也是用树结构来存放的, 任何编译程序都
会把源代码解析成一棵抽象语法树, 这样的表示法很恰当, 由于源代码就是层次结构的: 
函数包含参数和代码块, 代码快包含表达式和语句, 语句包含变量和运算符等等。

咱们已经知道, 任何树结构均可以垂手可得的写成XML, 而任何代码都会解析成树, 所以,
任何代码均可以转换成XML, 对不对? 我举个例子, 请看下面的函数:

int add(int arg1, int arg2)
{
    return arg1+arg2;
}

能把这个函数变成对等的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>

这个例子很是简单, 用哪一种语言来作都不会有太大问题。咱们能够把任何程序码转成XML,
也能够把XML转回到原来的程序码。咱们能够写一个转换器, 把Java代码转成XML, 另外一个
转换器把XML转回到Java。同样的道理, 这种手段也能够用来对付C++(这样作跟发疯差不
多么。但是的确有人在作, 看看GCC-XML(http://www.gccxml.org)就知道了)。进一步说,
凡有相同语言特性而语法不一样的语言, 均可以把XML看成中介来互相转换代码。实际上
几乎全部的主流语言都在必定程度上知足这个条件。咱们能够把XML做为一种中间表示法,
在两种语言之间互相译码。比方说, 咱们能够用Java2XML把Java代码转换成XML, 而后用
XML2CPP再把XML转换成C++代码, 运气好的话, 就是说, 若是咱们当心避免使用那些C++不
具有的Java特性的话, 咱们能够获得无缺的C++程序。这办法怎么样, 漂亮吧?

这一切充分说明, 咱们能够把XML做为源代码的通用存储方式, 其实咱们可以产生一整套
使用统一语法的程序语言, 也能写出转换器, 把已有代码转换成XML格式。若是真的采纳
这种办法, 各类语言的编译器就用不着本身写语法解析了, 它们能够直接用XML的语法解
析来直接生成抽象语法树。

说到这里你该问了, 咱们研究了这半天XML, 这和Lisp有什么关系呢? 毕竟XML出来之时,
Lisp早已经问世三十年了。这里我能够保证, 你立刻就会明白。不过在继续解释以前, 我
们先作一个小小的思惟练习。看一下上面这个XML版本的add函数例子, 你怎样给它分类, 
是代码仍是数据? 不用太多考虑都能明白, 把它分到哪一类都讲得通。它是XML, 它是标
准格式的数据。咱们也知道, 它能够经过内存中的树结构来生成(GCC-XML作的就是这个事
情)。它保存在不可执行的文件中。咱们能够把它解析成树节点, 而后作任意的转换。显
而易见, 它是数据。不过且慢, 虽然它语法有点陌生, 可它又确确实实是一个add函数, 
对吧?  一旦通过解析, 它就能够拿给编译器编译执行。咱们能够垂手可得写出这个XML 
代码解释器, 而且直接运行它。或者咱们也能够把它译成Java或C++代码, 而后再编译运
行。因此说, 它也是代码。

咱们说到那里了? 不错, 咱们已经发现了一个有趣的关键之点。过去被认为很难解的概念
已经很是直观很是简单的显现出来。代码也是数据, 而且历来都是如此。这听起来疯疯癫
癫的, 实际上倒是必然之事。我许诺过会以一种全新的方式来解释Lisp, 我要重申个人许
诺。可是咱们此刻尚未到预约的地方, 因此仍是先继续上边的讨论。

刚才我说过, 咱们能够很是简单地实现XML版的add函数解释器, 这听起来好像不过是说说
而已。谁真的会动手作一下呢? 未必有多少人会认真对待这件事。随便说说, 并不打算真
的去作, 这样的事情你在生活中恐怕也遇到吧。你明白我这样说的意思吧, 我说的有没有
打动你? 有哇, 那好, 咱们继续。

从新审视Ant

咱们如今已经来到了月亮背光的那一面, 先别忙着离开。再探索一下, 看看咱们还能发现
什么东西。闭上眼睛, 想想2000年冬天的那个雨夜, 一个名叫James Duncan Davidson 
的杰出的程序员正在研究Tomcat的servlet容器。那时, 他正当心地保存好刚修改过的文
件, 而后执行make。结果冒出了一大堆错误, 显然有什么东西搞错了。通过仔细检查, 他
想, 难道是由于tab前面加了个空格而致使命令不能执行吗? 确实如此。总是这样, 他真
的受够了。乌云背后的月亮给了他启示, 他建立了一个新的Java项目, 而后写了一个简单
可是十分有用的工具, 这个工具巧妙地利用了Java属性文件中的信息来构造工程, 如今
James能够写makefile的替代品, 它能起到相同的做用, 而形式更加优美, 也不用担忧有
makefile那样可恨的空格问题。这个工具可以自动解释属性文件, 而后采起正确的动做来
编译工程。真是简单而优美。

(做者注: 我不认识James, James也不认识我, 这个故事是根据网上关于Ant历史的帖子
虚构的)

使用Ant构造Tomcat以后几个月, 他愈来愈感到Java的属性文件不足以表达复杂的构造指
令。文件须要检出, 拷贝, 编译, 发到另一台机器, 进行单元测试。要是出错, 就发邮
件给相关人员, 要是成功, 就继续在尽量高层的卷(volumn)上执行构造。追踪到最后, 
卷要回复到最初的水平上。确实, Java的属性文件不够用了, James须要更有弹性的解决
方案。他不想本身写解析器(由于他更但愿有一个具备工业标准的方案)。XML看起来是个
不错的选择。他花了几天工夫把Ant移植到XML,因而,一件伟大的工具诞生了。

Ant是怎样工做的?原理很是简单。Ant把包含有构造命令的XML文件(算代码仍是算数据, 
你本身想吧),交给一个Java程序来解析每个元素,实际状况比我说的还要简单得多。
一个简单的XML指令会致使具备相同名字的Java类装入,并执行其代码。

    <copy todir="../new/dir">
        <fileset dir="src_dir" />
    </copy>

这段文字的含义是把源目录复制到目标目录,Ant会找到一个"copy"任务(实际上就是一个
Java类), 经过调用Java的方法来设置适当参数(todir和fileset),而后执行这个任务。
Ant带有一组核心类, 能够由用户任意扩展, 只要遵照若干约定就能够。Ant找到这些类, 
每当遇到XML元素有一样的名字, 就执行相应的代码。过程很是简单。Ant作到了咱们前面
所说的东西: 它是一个语言解释器, 以XML做为语法, 把XML元素转译为适当的Java指令。
咱们能够写一个"add"任务, 而后, 当发现XML中有add描述的时候, 就执行这个add任务。
因为Ant是很是流行的项目, 前面展现的策略就显得更为明智。毕竟, 这个工具天天差不
多有几千家公司在使用。

到目前为之, 我尚未说Ant在解析XML时所遇到困难。你也不用麻烦去它的网站上去找答
案了, 不会找到有价值的东西。至少对咱们这个论题来讲是如此。咱们仍是继续下一步讨
论吧。咱们答案就在那里。

为何是XML

有时候正确的决策并不是彻底出于深思熟虑。我不知道James选择XML是否出于深思熟虑。也
许仅仅是个下意识的决定。至少从James在Ant网站上发表的文章看起来, 他所说的理由完
全是似是而非。他的主要理由是移植性和扩展性, 在Ant案例上, 我看不出这两条有什么
帮助。使用XML而不是Java代码, 到底有什么好处? 为何不写一组Java类, 提供api来满
足基本任务(拷贝目录, 编译等等), 而后在Java里直接调用这些代码? 这样作仍然能够保
证移植性, 扩展性也是毫无疑问的。并且语法也更为熟悉, 看着顺眼。那为何要用 XML
呢? 有什么更好的理由吗?

有的。虽然我不肯定James是否确实意识到了。在语义的可构造性方面, XML的弹性是Java
可望不可即的。我不想用高深莫测的名词来吓唬你, 其中的道理至关简单, 解释起来并不费
不少功夫。好, 作好预备动做, 咱们立刻就要朝向顿悟的时刻作奋力一跃。

上面的那个copy的例子, 用Java代码怎样实现呢? 咱们能够这样作:

    CopyTask copy = new CopyTask();
    Fileset fileset = new Fileset();

    fileset.setDir("src_dir");
    copy.setToDir("../new/dir");
    copy.setFileset(fileset);

    copy.excute();

这个代码看起来和XML的那个很类似, 只是稍微长一点。差异在那里? 差异在于XML构造了
一个特殊的copy动词, 若是咱们硬要用Java来写的话, 应该是这个样子:

    copy("../new/dir");
    {
        fileset("src_dir");
    }

看到差异了吗? 以上代码(若是能够在Java中用的化), 是一个特殊的copy算符, 有点像
for循环或者Java5中的foreach循环。若是咱们有一个转换器, 能够把XML转换到Java, 大
概就会获得上面这段事实上不能够执行的代码。由于Java的技术规范是定死的, 咱们没有
办法在程序里改变它。咱们能够增长包, 增长类, 增长方法, 可是咱们没办法增长算符, 
而对于XML, 咱们显然能够任由本身增长这样的东西。对于XML的语法树来讲, 只要原意, 
咱们能够任意增长任何元素, 所以等于咱们能够任意增长算符。若是你还不太明白的话, 
看下面这个例子, 加入咱们要给Java引入一个unless算符:

    unless(someObject.canFly())
    {
        someObject.transportByGround():
    }

在上面的两个例子中, 咱们打算给Java语法扩展两个算符, 成组拷贝文件算符和条件算符
unless, 咱们要想作到这一点, 就必须修改Java编译器可以接受的抽象语法树, 显然咱们
没法用Java标准的功能来实现它。可是在XML中咱们能够垂手可得地作到。咱们的解析器
根据 XML元素, 生成抽象语法树, 由今生成算符, 因此, 咱们能够任意引入任何算符。

对于复杂的算符来讲, 这样作的好处显而易见。好比, 用特定的算符来作检出源码, 编译
文件, 单元测试, 发送邮件等任务, 想一想看有多么美妙。对于特定的题目, 好比说构造软
件项目, 这些算符的使用能够大幅减低少代码的数量。增长代码的清晰程度和可重用性。
解释性的XML能够很容易的达到这个目标。XML是存储层次化数据的简单数据文件, 而在
Java中, 因为层次结构是定死的(你很快就会看到, Lisp的状况与此大相径庭), 咱们就没
法达到上述目标。也许这正是Ant的成功之处呢。

你能够注意一下最近Java和C#的变化(尤为是C#3.0的技术规范), C#把经常使用的功能抽象出
来, 做为算符增长到C#中。C#新增长的query算符就是一个例子。它用的仍是传统的做法:
C#的设计者修改抽象语法树, 而后增长对应的实现。若是程序员本身也能修改抽象语法树
该有多好! 那样咱们就能够构造用于特定问题的子语言(好比说就像Ant这种用于构造项目
的语言), 你能想到别的例子吗? 再思考一下这个概念。不过也没必要思考太甚, 咱们待会
还会回到这个题目。那时候就会更加清晰。

离Lisp愈来愈近

咱们先把算符的事情放一放, 考虑一下Ant设计局限以外的东西。我早先说过, Ant能够通
过写Java类来扩展。Ant解析器会根据名字来匹配XML元素和Java类, 一旦找到匹配, 就执
行相应任务。为何不用Ant本身来扩展Ant呢? 毕竟核心任务要包含不少传统语言的结构
(例如"if"), 若是Ant自身就能提供构造任务的能力(而不是依赖java类), 咱们就能够得
到更高的移植性。咱们将会依赖一组核心任务(若是你原意, 也不妨把它称做标准库), 而
不用管有没有Java 环境了。这组核心任务能够用任何方式来实现, 而其余任务建筑在这
组核心任务之上, 那样的话, Ant就会成为通用的, 可扩展的, 基于XML的编程语言。考虑
下面这种代码的可能性:

    <task name="Test">
        <echo message="Hello World" />
    </task>
    <Test />

若是XML支持"task"的建立, 上面这段代码就会输出"Hello World!". 实际上, 咱们能够
用Java写个"task"任务, 而后用Ant-XML来扩展它。Ant能够在简单原语的基础上写出更复
杂的原语, 就像其余编程语言经常使用的做法同样。这也就是咱们一开始提到的基于XML的编
程语言。这样作用处不大(你知道为甚么吗?), 可是真的很酷。

再看一回咱们刚才说的Task任务。祝贺你呀, 你在看Lisp代码!!! 我说什么? 一点都不像
Lisp吗? 不要紧, 咱们再给它收拾一下。

比XML更好

前面一节说过, Ant自我扩展没什么大用, 缘由在于XML很烦琐。对于数据来讲, 这个问题
还不太大, 但若是代码很烦琐的话, 光是打字上的麻烦就足以抵消它的好处。你写过Ant 
的脚本吗? 我写过, 当脚本达到必定复杂度的时候, XML很是让人厌烦。想一想看吧, 为了
写结束标签, 每一个词都得打两遍, 不发疯算好的!

为了解决这个问题, 咱们应当简化写法。须知, XML仅仅是一种表达层次化数据的方式。
咱们并非必定要使用尖括号才能获得树的序列化结果。咱们彻底能够采用其余的格式。
其中的一种(恰好就是Lisp所采用的)格式, 叫作s表达式。s表达式要作的和XML同样, 但
它的好处是写法更简单, 简单的写法更适合代码输入。后面我会详细讲s表达式。这以前
我要清理一下XML的东西。考虑一下关于拷贝文件的例子:

    <copy toDir="../new/dir">
        <fileset dir="src_dir">
    </copy>

想一想看在内存里面, 这段代码的解析树在内存会是什么样子? 会有一个"copy"节点, 其下
有一个 "fileset"节点, 可是属性在哪里呢? 它怎样表达呢? 若是你之前用过XML, 而且
弄不清楚该用元素仍是该用属性, 你不用感到孤单, 别人同样糊涂着呢。没人真的搞得清
楚。这个选择与其说是基于技术的理由, 还不如说是闭着眼瞎摸。从概念上来说, 属性也
是一种元素, 任何属性能作的, 元素同样作获得。XML引入属性的理由, 其实就是为了让
XML写法不那么冗长。好比咱们看个例子:

    <copy>
        <toDir>../new/dir</toDir>
        <fileset>
            <dir>src_dir</dir>
        </fileset>
    </copy>

两下比较, 内容的信息量彻底同样, 用属性能够减小打字数量。若是XML没有属性的话, 
光是打字就够把人搞疯掉。

说完了属性的问题, 咱们再来看一看s表达式。之因此绕这么个弯, 是由于s表达式没有属
性的概念。由于s表达式很是简练, 根本没有必要引入属性。咱们在把XML转换成s表达式
的时候, 内心应该记住这一点。看个例子, 上面的代码译成s表达式是这样的:

    (copy 
        (todir "../new/dir")
        (fileset (dir "src_dir")))

仔细看看这个例子, 差异在哪里? 尖括号改为了圆括号, 每一个元素原来是有一对括号标记
包围的, 如今取消了后一个(就是带斜杠的那个)括号标记。表示元素的结束只须要一个")"
就能够了。不错, 差异就是这些。这两种表达方式的转换, 很是天然, 也很是简单。s表
达式打起字来, 也省事得多。第一次看s表达式(Lisp)时, 括号很烦人是吧? 如今咱们明
白了背后的道理, 一会儿就变得容易多了。至少, 比XML要好的多。用s表达式写代码, 不
单是实用, 并且也很让人愉快。s表达式具备XML的一切好处, 这些好处是咱们刚刚探讨过
的。如今咱们看看更加Lisp风格的task例子:

    (task (name "Test")
        (echo (message "Hellow World!")))
    (Test)

用Lisp的行话来说, s表达式称为表(list)。对于上面的例子, 若是咱们写的时候不加换
行, 用逗号来代替空格, 那么这个表达式看起来就很是像一个元素列表, 其中又嵌套着其
他标记。

    (task, (name, "test"), (echo, (message, "Hello World!")))

XML天然也能够用这样的风格来写。固然上面这句并非通常意义上的元素表。它实际上
是一个树。这和XML的做用是同样的。称它为列表, 但愿你不会感到迷惑, 由于嵌套表和
树其实是一码事。Lisp的字面意思就是表处理(list processing), 其实也能够称为树
处理, 这和处理XML节点没有什么不一样。

经受这一番折磨之后, 如今咱们终于至关接近Lisp了, Lisp的括号的神秘本质(就像许多
Lisp狂热分子认为的)逐渐显现出来。如今咱们继续研究其余内容。

从新审视C语言的宏

到了这里, 对XML的讨论你大概都听累了, 我都讲累了。咱们先停一停, 把树, s表达式,
Ant这些东西先放一放, 咱们来讲说C的预处理器。必定有人问了, 咱们的话题和C有什么
关系? 咱们已经知道了不少关于元编程的事情, 也探讨过专门写代码的代码。理解这问题
有必定难度, 由于相关讨论文章所使用的编程语言, 都是大家不熟悉的。可是若是只论概
念的话, 就相对要简单一些。我相信, 若是以C语言作例子来讨论元编程, 理解起来必定
会容易得多。好, 咱们接着看。

一个问题是, 为何要用代码来写代码呢? 在实际的编程中, 怎样作到这一点呢? 到底元
编程是什么意思? 你大概已经据说过这些问题的答案, 可是并不懂得其中原因。为了揭示
背后的真理, 咱们来看一下一个简单的数据库查询问题。这种题目咱们都作过。比方说, 
直接在程序码里处处写SQL语句来修改表(table)里的数据, 写多了就很是烦人。即使用
C#3.0的LINQ, 仍然不减其痛苦。写一个完整的SQL查询(尽管语法很优美)来修改某人的地
址, 或者查找某人的名字, 绝对是件令程序员倍感乏味的事情, 那么咱们该怎样来解决这
个问题? 答案就是: 使用数据访问层。 

概念挺简单, 其要点是把数据访问的内容(至少是那些比较琐碎的部分)抽象出来, 用类来
映射数据库的表, 而后用访问对象属性访问器(accessor)的办法来间接实现查询。这样就
极大地简化了开发工做量。咱们用访问对象的方法(或者属性赋值, 这要视你选用的语言
而定)来代替写SQL查询语句。凡是用过这种方法的人, 都知道这很节省时间。固然, 若是
你要亲自写这样一个抽象层, 那但是要花很是多的时间的--你要写一组类来映射表, 把属
性访问转换为SQL查询, 这个活至关耗费精力。用手工来作显然是很不明智的。可是一旦
你有了方案和模板, 实际上就没有多少东西须要思考的。你只须要按照一样的模板一次又
一次重复编写类似代码就能够了。事实上不少人已经发现了更好的方法, 有一些工具能够
帮助你链接数据库, 抓取数据库结构定义(schema), 按照预约义的或者用户定制的模板来
自动编写代码。

若是你用过这种工具, 你确定会对它的神奇效果深为折服。每每只须要鼠标点击数次, 就
能够链接到数据库, 产生数据访问源码, 而后把文件加入到你的工程里面, 十几分钟的工
做, 按照往常手工方式来做的话, 也许须要数百个小时人工(man-hours)才能完成。但是,
若是你的数据库结构定义后来改变了怎么办? 那样的话, 你只需把这个过程重复一遍就可
以了。甚至有一些工具能自动完成这项变更工做。你只要把它做为工程构造的一部分, 每
次编译工程的时候, 数据库部分也会自动地从新构造。这真的太棒了。你要作的事情基本
上减到了0。若是数据库结构定义发生了改变, 并在编译时自动更新了数据访问层的代码,
那么程序中任何使用过期的旧代码的地方, 都会引起编译错误。

数据访问层是个很好的例子, 这样的例子还有好多。从GUI样板代码, WEB代码, COM和
CORBA存根, 以及MFC和ATL等等。在这些地方, 都是有好多类似代码屡次重复。既然这些
代码有可能自动编写, 而程序员时间又远远比CPU时间昂贵, 固然就产生了好多工具来自
动生成样板代码。这些工具的本质是什么呢? 它们实际上就是制造程序的程序。它们有一
个神秘的名字, 叫作元编程。所谓元编程的本义, 就是如此。

元编程原本能够用到无数多的地方, 但实际上使用的次数却没有那么多。归根结底, 咱们
内心仍是在盘算, 假设重复代码用拷贝粘贴的话, 大概要重复6,7次, 对于这样的工做量,
值得专门创建一套生成工具吗? 固然不值得。数据访问层和COM存根每每须要重用数百次,
甚至上千次, 因此用工具生成是最好的办法。而那些仅仅是重复几回十几回的代码, 是没
有必要专门作工具的。没必要要的时候也去开发代码生成工具, 那就显然过分估计了代码生
成的好处。固然, 若是建立这类工具足够简单的话, 仍是应当尽可能多用, 由于这样作必然
会节省时间。如今来看一下有没有合理的办法来达到这个目的。

如今, C预处理器要派上用场了。咱们都用过C/C++的预处理器, 咱们用它执行简单的编译
指令, 来产生简单的代码变换(比方说, 设置调试代码开关), 看一个例子:

    #define triple(X) X+X+X

这一行的做用是什么? 这是一个简单的预编译指令, 它把程序中的triple(X)替换称为
X+X+X。例如, 把全部的triple(5)都换成5+5+5, 而后再交给编译器编译。这就是一个简
单的代码生成的例子。要是C的预处理器再强大一点, 要是可以容许链接数据库, 要是能
多一些其余简单的机制, 咱们就能够在咱们程序的内部开发本身的数据访问层。下面这个
例子, 是一个假想的对C宏的扩展:

    #get-db-schema("127.0.0.1")
    #iterate-through-tables
    #for-each-table
        class #table-name
            {
            };
    #end-for-each

咱们链接数据库结构定义, 遍历数据表, 而后对每一个表建立一个类, 只消几行代码就完成
了这个工做。这样每次编译工程的时候, 这些类都会根据数据库的定义同步更新。显而易
见, 咱们不费吹灰之力就在程序内部创建了一个完整的数据访问层, 根本用不着任何外部
工具。固然这种做法有一个缺点, 那就是咱们得学习一套新的"编译时语言", 另外一个缺点
就是根本不存在这么一个高级版的C预处理器。须要作复杂代码生成的时候, 这个语言(译
者注: 这里指预处理指令, 即做者所说的"编译时语言")自己也必定会变得至关复杂。它
必须支持足够多的库和语言结构。好比说咱们想要生成的代码要依赖某些ftp服务器上的
文件, 预处理器就得支持ftp访问, 仅仅由于这个任务而不得不创造和学习一门新的语言,
真是有点让人恶心(事实上已经存在着有此能力的语言, 这样作就更显荒谬)。咱们不妨再
灵活一点, 为何不直接用 C/C++本身做为本身的预处理语言呢?  这样子的话, 咱们可
以发挥语言的强大能力, 要学的新东西也只不过是几个简单的指示字 , 这些指示字用来
区别编译时代码和运行时代码。

    <%
        cout<<"Enter a number: ";
        cin>>n;
    %>
    for(int i=0;i< <% n %>;i++)
    {
        cout<<"hello"<<endl;
    }

你明白了吗? 在<%和%>标记之间的代码是在编译时运行的, 标记以外的其余代码都是普通
代码。编译程序时, 系统会提示你输入一个数, 这个数在后面的循环中会用到。而for循
环的代码会被编译。假定你在编译时输入5, for循环的代码将会是:

    for(int i=0;i<5; i++)
    {
        cout<<"hello"<<endl;
    }

又简单又有效率, 也不须要另外的预处理语言。咱们能够在编译时就充分发挥宿主语言( 
此处是C/C++)的强大能力, 咱们能够很容易地在编译时链接数据库, 创建数据访问层, 就
像JSP或者ASP建立网页那样。咱们也用不着专门的窗口工具来另外创建工程。咱们能够在
代码中当即加入必要的工具。咱们也用不着顾虑创建这种工具是否是值得, 由于这太容易
了, 太简单了。这样子不知能够节省多少时间啊。

你好, Lisp

到此刻为止, 咱们所知的关于Lisp的指示能够总结为一句话: Lisp是一个可执行的语法更
优美的XML, 但咱们尚未说Lisp是怎样作到这一点的, 如今开始补上这个话题。 

Lisp有丰富的内置数据类型, 其中的整数和字符串和其余语言没什么分别。像71或者
"hello"这样的值, 含义也和C++或者Java这样的语言大致相同。真正有意思的三种类型是
符号(symbol), 表和函数。这一章的剩余部分, 我都会用来介绍这几种类型, 还要介绍
Lisp环境是怎样编译和运行源码的。这个过程用Lisp的术语来讲一般叫作求值。通读这一
节内容, 对于透彻理解元编程的真正潜力, 以及代码和数据的同一性, 和面向领域语言的
观念, 都极其重要。万勿等闲视之。我会尽可能讲得生动有趣一些, 也但愿你能得到一些
启发。那好, 咱们先讲符号。

大致上, 符号至关于C++或Java语言中的标志符, 它的名字能够用来访问变量值(例如
currentTime, arrayCount, n, 等等), 差异在于, Lisp中的符号更加基本。在C++或
Java里面, 变量名只能用字母和下划线的组合, 而Lisp的符号则很是有包容性, 好比, 加
号(+)就是一个合法的符号, 其余的像-, =, hello-world, *等等均可以是符号名。符号
名的命名规则能够在网上查到。你能够给这些符号任意赋值, 咱们这里先用伪码来讲明这
一点。假定函数set是给变量赋值(就像等号=在C++和Java里的做用), 下面是咱们的例子:

    set(test, 5)            // 符号test的值为5
    set(=, 5)               // 符号=的值为5
    set(test, "hello")      // 符号test的值为字符串"hello"
    set(test, =)            // 此时符号=的值为5, 因此test的也为5
    set(*, "hello")         // 符号*的值为"hello"

好像有什么不对的地方? 假定咱们对*赋给整数或者字符串值, 那作乘法时怎么办? 无论
怎么说, *老是乘法呀? 答案简单极了。Lisp中函数的角色十分特殊, 函数也是一种数据
类型, 就像整数和字符串同样, 所以能够把它赋值给符号。乘法函数Lisp的内置函数, 默
认赋给*, 你能够把其余函数赋值给*, 那样*就不表明乘法了。你也能够把这函数的值存
到另外的变量里。咱们再用伪码来讲明一下:

    *(3,4)          // 3乘4, 结果是12
    set(temp, *)    // 把*的值, 也就是乘法函数, 赋值给temp
    set(*, 3)       // 把3赋予*
    *(3,4)          // 错误的表达式, *再也不是乘法, 而是数值3
    temp(3,4)       // temp是乘法函数, 因此此表达式的值为3乘4等于12
    set(*, temp)    // 再次把乘法函数赋予*
    *(3,4)          // 3乘4等于12

再古怪一点, 把减号的值赋给加号:

    set(+, -)       // 减号(-)是内置的减法函数
    +(5, 4)         // 加号(+)如今是表明减法函数, 结果是5减4等于1

这只是举例子, 我尚未详细讲函数。Lisp中的函数是一种数据类型, 和整数, 字符串, 
符号等等同样。一个函数并没必要然有一个名字, 这和C++或者Java语言的情形很不相同。
在这里函数本身表明本身。事实上它是一个指向代码块的指针, 附带有一些其余信息(例
如一组参数变量)。只有在把函数赋予其余符号时, 它才具备了名字, 就像把一个数值或
字符串赋予变量同样的道理。你能够用一个内置的专门用于建立函数的函数来建立函数,
而后把它赋值给符号fn, 用伪码来表示就是:

    fn [a]
    {
        return *(a, 2);
    }

这段代码返回一个具备一个参数的函数, 函数的功能是计算参数乘2的结果。这个函数还
没有名字, 你能够把此函数赋值给别的符号:

    set(times-two, fn [a] {return *(a, 2)})

咱们如今能够这样调用这个函数:

    time-two(5)         // 返回10

咱们先跳过符号和函数, 讲一讲表。什么是表? 你也许已经听过好多相关的说法。表, 一
言以蔽之, 就是把相似XML那样的数据块, 用s表达式来表示。表用一对括号括住, 表中元
素以空格分隔, 表能够嵌套。例如(这回咱们用真正的Lisp语法, 注意用分号表示注释):

    ()                      ; 空表
    (1)                     ; 含一个元素的表
    (1 "test")              ; 两元素表, 一个元素是整数1, 另外一个是字符串
    (test "hello")          ; 两元素表, 一个元素是符号, 另外一个是字符串
    (test (1 2) "hello")    ; 三元素表, 一个符号test, 一个含有两个元素1和2的
                            ; 表, 最后一个元素是字符串

当Lisp系统遇到这样的表时, 它所作的, 和Ant处理XML数据所作的, 很是类似, 那就是试
图执行它们。其实, Lisp源码就是特定的一种表, 比如Ant源码是一种特定的XML同样。
Lisp执行表的顺序是这样的, 表的第一个元素看成函数, 其余元素看成函数的参数。若是
其中某个参数也是表, 那就按照一样的原则对这个表求值, 结果再传递给最初的函数做为
参数。这就是基本原则。咱们看一下真正的代码:

    (* 3 4)                 ; 至关于前面列举过的伪码*(3,4), 即计算3乘4
    (times-two 5)           ; 返回10, times-two按照前面的定义是求参数的2倍
    (3 4)                   ; 错误, 3不是函数
    (time-two)              ; 错误, times-two要求一个参数
    (times-two 3 4)         ; 错误, times-two只要求一个参数
    (set + -)               ; 把减法函数赋予符号+
    (+ 5 4)                 ; 依据上一句的结果, 此时+表示减法, 因此返回1
    (* 3 (+ 2 2))           ; 2+2的结果是4, 再乘3, 结果是12

上述的例子中, 全部的表都是看成代码来处理的。怎样把表看成数据来处理呢? 一样的,
设想一下, Ant是把XML数据看成本身的参数。在Lisp中, 咱们给表加一个前缀'来表示数
据。

    (set test '(1 2))       ; test的值为两元素表
    (set test (1 2))        ; 错误, 1不是函数
    (set test '(* 3 4))     ; test的值是三元素表, 三个元素分别是*, 3, 4

咱们能够用一个内置的函数head来返回表的第一个元素, tail函数来返回剩余元素组成的
表。

    (head '(* 3 4))         ; 返回符号*
    (tail '(* 3 4))         ; 返回表(3 4)
    (head (tal '(* 3 4)))   ; 返回3
    (head test)             ; 返回*

你能够把Lisp的内置函数想像成Ant的任务。差异在于, 咱们不用在另外的语言中扩展
Lisp(虽然彻底能够作获得), 咱们能够用Lisp本身来扩展本身, 就像上面举的times-two
函数的例子。Lisp的内置函数集十分精简, 只包含了十分必要的部分。剩下的函数都是做
为标准库来实现的。

Lisp宏

咱们已经看到, 元编程在一个相似jsp的模板引擎方面的应用。咱们经过简单的字符串处
理来生成代码。可是咱们能够作的更好。咱们先提一个问题, 怎样写一个工具, 经过查找
目录结构中的源文件来自动生成Ant脚本。

用字符串处理的方式生成Ant脚本是一种简单的方式。固然, 还有一种更加抽象, 表达能
力更强, 扩展性更好的方式, 就是利用XML库在内存中直接生成XML节点, 这样的话内存中
的节点就能够自动序列化成为字符串。不只如此, 咱们的工具还能够分析这些节点, 对已
有的XML文件作变换。经过直接处理XML节点。咱们能够超越字符串处理, 使用更高层次的
概念, 所以咱们的工做就会作的更快更好。

咱们固然能够直接用Ant自身来处理XML变换和制做代码生成工具。或者咱们也能够用Lisp
来作这项工做。正像咱们之前所知的, 表是Lisp内置的数据结构, Lisp含有大量的工具来
快速有效的操做表(head和tail是最简单的两个)。并且, Lisp没有语义约束, 你能够构造
任何数据结构, 只要你原意。

Lisp经过宏(macro)来作元编程。咱们写一组宏来把任务列表(to-do list)转换为专用领
域语言。

回想一下上面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>

相应的s表达式是这样的:

    (todo "housework"
        (item (priority high) "Clean the house")
        (item (priority medium) "Wash the dishes")
        (item (priority medium) "Buy more soap"))

假设咱们要写一个任务表的管理程序, 把任务表数据存到一组文件里, 当程序启动时, 从
文件读取这些数据并显示给用户。在别的语言里(好比说Java), 这个任务该怎么作? 咱们
会解析XML文件, 从中得出任务表数据, 而后写代码遍历XML树, 再转换为Java的数据结构
(老实讲, 在Java里解析XML真不是件轻松的事情), 最后再把数据展现给用户。如今若是
用Lisp, 该怎么作?

假定要用一样思路的化, 咱们大概会用Lisp库来解析XML。XML对咱们来讲就是一个Lisp 
的表(s表达式), 咱们能够遍历这个表, 而后把相关数据提交给用户。但是, 既然咱们用
Lisp, 就根本没有必要再用XML格式保存数据, 直接用s表达式就行了, 这样就没有必要作
转换了。咱们也用不着专门的解析库, Lisp能够直接在内存里处理s表达式。注意, Lisp 
编译器和.net编译器同样, 对Lisp程序来讲, 在运行时老是随时可用的。

可是还有更好的办法。咱们甚至不用写表达式来存储数据, 咱们能够写宏, 把数据看成代
码来处理。那该怎么作呢? 真的简单。回想一下, Lisp的函数调用格式:

    (function-name arg1 arg2 arg3)

其中每一个参数都是s表达式, 求值之后, 传递给函数。若是咱们用(+ 4 5)来代替arg1, 
那么, 程序会先求出结果, 就是9, 而后把9传递给函数。宏的工做方式和函数相似。主要
的差异是, 宏的参数在代入时不求值。

    (macro-name (+ 4 5))

这里, (+ 4 5)做为一个表传递给宏, 而后宏就能够任意处理这个表, 固然也能够对它求
值。宏的返回值是一个表, 而后有程序做为代码来执行。宏所占的位置, 就被替换为这个
结果代码。咱们能够定义一个宏把数据替换为任意代码, 比方说, 替换为显示数据给用户
的代码。

这和元编程, 以及咱们要作的任务表程序有什么关系呢? 实际上, 编译器会替咱们工做, 
调用相应的宏。咱们所要作的, 仅仅是建立一个把数据转换为适当代码的宏。

例如, 上面曾经将过的C的求三次方的宏, 用Lisp来写是这样子:

    (defmacro triple (x)
        `(+ ~x ~x ~x))

(译注: 在Common Lisp中, 此处的单引号应当是反单引号, 意思是对表不求值, 但能够对
表中某元素求值, 记号~表示对元素x求值, 这个求值记号在Common Lisp中应当是逗号。
反单引号和单引号的区别是, 单引号标识的表, 其中的元素都不求值。这里做者所用的记
号是本身发明的一种Lisp方言Blaise, 和common lisp略有不一样, 事实上, 发明方言是
lisp高手独有的乐趣, 不少狂热分子都热衷这样作。好比Paul Graham就发明了ARC, 许多
记号比传统的Lisp简洁得多, 显得比较现代)

单引号的用处是禁止对表求值。每次程序中出现triple的时候, 

    (triple 4)

都会被替换成:

    (+ 4 4 4)

咱们能够为任务表程序写一个宏, 把任务数据转换为可执行码, 而后执行。假定咱们的输
出是在控制台:

    (defmacro item (priority note)
        `(block 
            (print stdout tab "Prority: " ~(head (tail priority)) endl)
            (print stdout tab "Note: " ~note endl endl)))

咱们创造了一个很是小的有限的语言来管理嵌在Lisp中的任务表。这个语言只用来解决特
定领域的问题, 一般称之为DSLs(特定领域语言, 或专用领域语言)。

特定领域语言

本文谈到了两个特定领域语言, 一个是Ant, 处理软件构造。一个是没起名字的, 用于处
理任务表。二者的差异在于, Ant是用XML, XML解析器, 以及Java语言合在一块儿构造出来
的。而咱们的迷你语言则彻底内嵌在Lisp中, 只消几分钟就作出来了。

咱们已经说过了DSL的好处, 这也就是Ant用XML而不直接用Java的缘由。若是使用Lisp, 
咱们能够任意建立DSL, 只要咱们须要。咱们能够建立用于网站程序的DSL, 能够写多用户
游戏, 作固定收益贸易(fixed income trade), 解决蛋白质折叠问题, 处理事务问题, 等
等。咱们能够把这些叠放在一块儿, 造出一个语言, 专门解决基于网络的贸易程序, 既有网
络语言的优点, 又有贸易语言的好处。天天咱们都会收获这种方法带给咱们的益处, 远远
超过Ant所能给予咱们的。

用DSL解决问题, 作出的程序精简, 易于维护, 富有弹性。在Java里面, 咱们能够用类来
处理问题。这两种方法的差异在于, Lisp使咱们达到了一个更高层次的抽象, 咱们再也不受
语言解析器自己的限制, 比较一下用Java库直接写的构造脚本和用Ant写的构造脚本其间
的差异。一样的, 比较一下你之前所作的工做, 你就会明白Lisp带来的好处。

接下来

学习Lisp就像战争中争夺山头。尽管在电脑科学领域, Lisp已经算是一门古老的语言, 直
到如今仍然不多有人真的明白该怎样给初学者讲授Lisp。尽管Lisp老手们尽了很大努力,
今天新手学习Lisp仍然是困难重重。好在如今事情正在发生变化, Lisp的资源正在迅速增
加, 随着时间推移, Lisp将会愈来愈受关注。

Lisp令人超越平庸, 走到前沿。学会Lisp意味着你能找到更好的工做, 由于聪明的雇主会
被你不同凡响的洞察力所打动。学会Lisp也可能意味着明天你可能会被解雇, 由于你老是
强调, 若是公司全部软件都用Lisp写, 公司将会如何卓越, 而这些话你的同事会听烦的。
Lisp值得努力学习吗? 那些已经学会Lisp的人都说值得, 固然, 这取决于你的判断。

你的见解呢?

这篇文章写写停停, 用了几个月才最终完成。若是你以为有趣, 或者有什么问题, 意见或
建议, 请给我发邮件coffeemug@gmail.com, 我会很高兴收到你的反馈。html

相关文章
相关标签/搜索