Lisp 的本质(2)

来源:互联网 发布:java short类型 编辑:程序博客网 时间:2024/05/22 11:36

你好, 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)          // 34, 结果是12

set(temp, *)    // *的值, 也就是乘法函数, 赋值给temp

set(*3)       // 3赋予*

*(3,4)          // 错误的表达式, *不再是乘法, 而是数值3

temp(3,4)       // temp是乘法函数, 所以此表达式的值为34等于12

set(*, temp)    // 再次把乘法函数赋予*

*(3,4)          // 34等于12


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


set(+-)       // 减号(-)是内置的减法函数

+(54)         // 加号(+)现在是代表减法函数, 结果是54等于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, 一个含有两个元素12

; 表, 最后一个元素是字符串


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


(* 3 4)                 ; 相当于前面列举过的伪码*(3,4), 即计算34

(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的值是三元素表, 三个元素分别是*34


我们可以用一个内置的函数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 (+45))


这里, (+ 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)


都会被替换成:


(+444)


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


(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的人都说值得, 当然, 这取决于你的判断。



英文:defmacro

译文:Alpha(奥法)

http://www.cnblogs.com/Leap-abead/articles/762180.html


0 0