又简单又有效率, 也不需要另外的预处理语言。我们可以在编译时就充分发挥宿主语言(此处是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的数据格式是这样的:

Clean the hose

Wash the dishes

Buy more soap

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

着墨中文lisp登入_Lisp的本质 - climbdream的个人空间 - OSCHINA - 中文开源技术交流社区...相关推荐

  1. Linux环境变量隔代,python基础题 - osc_vwtuqll7的个人空间 - OSCHINA - 中文开源技术交流社区...

    47.metaclass作用?以及应用场景? metaclass,直译为元类,简单的解释就是:当我们定义了类以后,就可以根据这个类创建出实例, 所以:先定义类,然后创建实例.但是如果我们想创建出类呢? ...

  2. java 锁的类型_Java锁的种类 - shawnplaying的个人页面 - OSCHINA - 中文开源技术交流社区...

    Java锁和并发需要结合在一块了理解,涉及到了多个话题. 本文主要参考了 http://ifeve.com/java_lock_see1/ 但是我认为原文中有某些错误,我在下面的代码中做了修改. 公平 ...

  3. 对linux的mv命令设计测试用例,测试用例中的细节 - 八音弦的个人空间 - OSCHINA - 中文开源技术交流社区...

    编写测试用例是在实际测试执行开始之前进行的软件测试活动的重要组成部分.因此,在编写测试用例时必须头脑清晰地理解需求.测试执行阶段的顺利程度主要取决于测试用例的编写质量,还取决于对需求的理解程度.理论上 ...

  4. python允许无止境的循环吗_Python第一天 - 思想永无止境的个人页面 - OSCHINA - 中文开源技术交流社区...

    下载安装python以及相关软件. python 直接到python官网下windows-msi文件,版本根据需要下2.7或3.0. pydev 自己网上搜,pydev官网的反而不行(eclipse上 ...

  5. 学数据结构堆襸_gbk编码 - osc_6pogm9r5的个人空间 - OSCHINA - 中文开源技术交流社区...

    GBK编码范围:8140-FEFE,汉字编码范围见第二节:码位分配及顺序. GBK编码,是对GB2312编码的扩展,因此完全兼容GB2312-80标准.GBK编码依然采用双字节编码方案,其编码范围:8 ...

  6. c语言问答题斐讯通信,C总结-part_1 - LinearLaw的个人空间 - OSCHINA - 中文开源技术交流社区...

    1_C语言概述 1.库引用 1.1.几个例子 例1,调用系统指令 // b.c #include int main(void) { // system函数,相当于在命令行界面中输入对应的命令 syst ...

  7. php isscalar,1+X PHP知识 - osc_2g1pfov3的个人空间 - OSCHINA - 中文开源技术交流社区

    #PHP技术与应用 ##第一章  网站介绍 ###第一节 动态网站 概念: 误区:不是指网站当中包含动态图片.滚动图等动态效果. 正解:采用数据库技术开发的网站,网页上的内容都是通过数据库提取出来动态 ...

  8. 利用mysql建立随机森林_随机森林算法实例 - osc_4imme0wh的个人空间 - OSCHINA - 中文开源技术交流社区...

    根据成年人数据集来预测一个人的收入 1.准备数据集 我下载好了一个成年人数据集,从百度云下载 链接:https://pan.baidu.com/s/10gC8U0tyh1ERxLhtY8i0bQ 提取 ...

  9. 黑白球JAVA_桶中取黑白球 - jiacut的个人页面 - OSCHINA - 中文开源技术交流社区

    题目 有一个桶,里面有白球.黑球各100个,现在按下述规则取球: 每次从桶里面拿出来两个球: 如果是两个同色的球,就再放入一个黑球: 如果是两个异色的球,就再放入一个白球. 问:最后桶里面只剩下一个黑 ...

最新文章

  1. Codeforces Round 367 Div. 2
  2. 你的气质里藏着 英文_有小肚腩女人穿衣要讲究,针织套装裙洋气又时髦,穿出优雅气质...
  3. gnuradio 初次使用
  4. 学习Java笔记(一)
  5. 阿里云rds for mysql平台介绍_阿里云RDS for MySQL 快速入门——笔记
  6. Unity资源管理--AssetBundle学习
  7. Ccover在hudson持续集成中的应用
  8. Java中hashCode和equals方法的正确使用
  9. blog群发王(价值1980元)源代码提供
  10. 使用预计算实时全局光照优化照明-项目介绍
  11. Mysql表数据如何增加汇总统计行(GROUP BY WITH ROLLUP函数用法)
  12. 冒泡排序及一个小小的优化
  13. 全面解读人工智能、大数据和云计算的关系
  14. 实现导航栏的几种方式
  15. 单片机应用系统设计技术——基于51单片机的火灾温度烟雾报警器设计
  16. OpenGL多重纹理使用与理解
  17. Vue3必会技巧-自定义Hooks
  18. 51 PWM调速蓝牙小车
  19. wx.showModal(模态框)的相关设置
  20. Bootstrap系列之treeview实现菜单树

热门文章

  1. python摄像头推流_树莓派使用python-librtmp实现rtmp推流h264的方法
  2. python工程技巧_python 19个值得学习的编程技巧
  3. 华为ipd产品开发流程_亲历华为IPD变革是怎样一种体验|附完整版培训教材
  4. 【BUG记录】Matisse显示的图片乱序或者在全部项不显示
  5. 东北大学浑南校区计算机学院,浑南校区各主要建筑介绍:信息科学大楼
  6. android cad 开源库,KiCad 开源元件库收集
  7. OSG官方自带的例子程序简介
  8. Android开发之RecyclerView嵌套ListView自动计算高度的方法
  9. [51nod] 1301 集合异或和
  10. ES6 Proxy兼容polyfill实现