Lisp 已死,Lisp 万岁!

有一句古话,叫做“国王已死,国王万岁!”它的意思是,老国王已经死去,国王的儿子现在继位。这句话的幽默,就在于这两个“国王”其实指的不是同一个人,而你咋一看还以为它自相矛盾。今天我的话题仿效了这句话,叫做“Lisp 已死,Lisp 万岁!”希望到最后你会明白这是什么意思。

首先,我想总结一下 Lisp 的优点。你也许已经知道,Lisp 身上最重要的一些优点,其实已经“遗传”到了几乎每种流行的语言身上(Java,C#,JavaScript,Python, Ruby,Haskell,……)。由于我已经在其他博文里详细的叙述过其中一些,所以现在只把这些 Lisp 的优点简单列出来(关键部分加了链接):

  • Lisp 的语法是世界上最精炼,最美观,也是语法分析起来最高效的语法。这是 Lisp 独一无二的,其他语言都没有的优点。有些人喜欢设计看起来很炫的语法,其实都是自找麻烦。为什么这么说呢,请参考这篇《谈语法》。

  • Lisp 是第一个可以在程序的任何位置定义函数,并且可以把函数作为值传递的语言。这样的设计使得它的表达能力非常强大。这种理念被 Python,JavaScript,Ruby 等语言所借鉴。

  • Lisp 有世界上最强大的宏系统(macro system)。这种宏系统的表达力几乎达到了理论所允许的极限。如果你只见过 C 语言的“宏”,那我可以告诉你它是完全没法跟 Lisp 的宏系统相提并论的。

  • Lisp 是世界上第一个使用垃圾回收(garbage collection)的语言。这种超前的理念,后来被 Java,C# 等语言借鉴。

想不到吧,现代语言的很多优点,其实都是来自于 Lisp — 世界上第二古老的程序语言。所以有人才会说,每一种现代语言都在朝着 Lisp 的方向“进化”。如果你相信了这话,也许就会疑惑,为什么 Lisp 今天没有成为主流,为什么 Lisp Machine 会被 Unix 打败。其实除了商业原因之外,还有技术上的问题。

早期的 Lisp 其实普遍存在一个非常严重的问题:它使用 dynamic scoping。所谓 dynamic scoping 就是说,如果你的函数定义里面有“自由变量”,那么这个自由变量的值,会随着函数的“调用位置”的不同而发生变化。

比如下面我定义一个函数 f,它接受一个参数 y,然后返回 x 和 y 的积。

(setq f
(let ((x 1))
(lambda (y) (* x y))))

这里 x 对于函数 (lambda (y) (* x y)) 来说是个“自由变量”(free variable),因为它不是它的参数。

看着这段代码,你会很自然的认为,因为 x 的值是 1,那么 f 被调用的时候,结果应该等于 (* 1 y),也就是说应该等于 y 的值。可是这在 dynamic scoping 的语言里结果如何呢?我们来看看吧。

(你可以在 emacs 里面试验以下的结果,因为 Emacs Lisp 使用的就是 dynamic scoping。)

如果我们在函数调用的外层定义一个 x,值为 2:

(let ((x 2))
(funcall f 2))

因为这个 x 跟 f 定义处的 x 的作用域不同,所以它们不应该互相干扰。所以我们应该得到 2。可是,这段代码返回的结果却为 4。

再来。我们另外定义一个 x,值为 3:

(let ((x 3))
(funcall f 2))

我们的期望值还是 2,可是结果却是 6。

再来。如果我们直接调用:

(funcall f 2)

你想这次总该得到 2 了吧?结果,出错了:

Debugger entered--Lisp error: (void-variable x)
(* x y)
(lambda (y) (* x y))(2)
funcall((lambda (y) (* x y)) 2)
eval_r((funcall f 2) nil)
eval-last-sexp-1(nil)
eval-last-sexp(nil)
call-interactively(eval-last-sexp nil nil)

看到问题了吗?f 的行为,随着调用位置的一个“名叫 x”的变量的值而发生变化。而这个 x,跟 f 定义处的 x 其实根本就不是同一个变量,它们只不过名字相同而已。这会导致非常难以发现的错误,也就是早期的 Lisp 最令人头痛的地方。好在现在的大部分语言其实已经吸取了这个教训,所以你不再会遇到这种让人发疯的痛苦。不管是 Scheme, Common Lisp, Haskell, OCaml, Python, JavaScript…… 都不使用 dynamic scoping。

那现在也许你了解了,什么是让人深恶痛绝的 dynamic scoping。如果我告诉你,Lisp Machine 所使用的语言 Lisp Machine Lisp 使用的也是 dynamic scoping,你也许就明白了为什么 Lisp Machine 会失败。因为它跟现在的 Common Lisp 和 Scheme,真的是天壤之别。我宁愿写 C++,Java 或者 Python,也不愿意写 Lisp Machine Lisp 或者 Emacs Lisp。

话说回来,为什么早期的 Lisp 会使用 dynamic scoping 呢?其实这根本就不是一个有意的“设计”,而是一个无意的“巧合”。你几乎什么都不用做,它就成那个样子了。这不是开玩笑,如果你在 emacs 里面显示 f 的值,它会打印出:

'(lambda (y) (* x y))

这说明 f 的值其实是一个 S 表达式,而不是像 Scheme 一样的“闭包”(closure)。原来,Emacs Lisp 直接把函数定义处的 S 表达式 ‘(lambda (y) (* x y)) 作为了函数的“值”,这是一种很幼稚的做法。如果你是第一次实现函数式语言的新手,很有可能就会这样做。Lisp 的设计者当年也是这样的情况。

简单倒是简单,麻烦事接着就来了。调用 f 的时候,比如 (funcall f 2),y 的值当然来自参数 2,可是 x 的值是多少呢?答案是:不知道!不知道怎么办?到“外层环境”去找呗,看到哪个就用哪个,看不到就报错。所以你就看到了之前出现的现象,函数的行为随着一个完全无关的变量而变化。如果你单独调用 (funcall f 2) 就会因为找不到 x 的值而出错。

那么正确的实现函数的做法是什么呢?是制造“闭包”(closure)。这也就是 Scheme,Common Lisp 以及 Python,C# 的做法。在函数定义被解释或者编译的时候,当时的自由变量(比如 x)的值,会跟函数的代码绑在一起,被放进一种叫做“闭包”的结构里。比如上面的函数,就可以表示成这个样子:(Closure ‘(lambda (y) (* x y)) ‘((x . 1)))。

在这里我用 (Closure …) 表示一个“结构”(就像 C 语言的 struct)。它的第一个部分,是这个函数的定义。第二个部分是 ‘((x . 1)),它是一个“环境”,其实就是一个从变量到值的映射(map)。利用这个映射,我们记住函数定义处的那个 x 的值,而不是在调用的时候才去瞎找。

我不想在这里深入细节。如果你对实现语言感兴趣的话,可以参考我的另一篇博文《怎样写一个解释器》。它教你如何实现一个正确的,没有以上毛病的解释器。

与 dynamic scoping 相对的就是“lexical scoping”。我刚才告诉你的闭包,就是 lexical scoping 的实现方法。第一个实现 lexical scoping 的语言,其实不是 Lisp 家族的,而是 Algol 60。“Algol”之所以叫这名字,是因为它的设计初衷是用来实现算法(algorithm)。其实 Algol 比起 Lisp 有很多不足,但在 lexical scoping 这一点上它却做对了。Scheme 从 Algol 60 身上学到了 lexical scoping,成为了第一个使用 lexical scoping 的“Lisp 方言”。9 年之后,Lisp 家族的“集大成者” Common Lisp 诞生了,它也采用了 lexical scoping。看来英雄所见略同。

你也许发现了,Lisp 其实不是一种语言,而是很多种语言。这些被人叫做“Lisp 家族”的语言,其实共同点只是它们的“语法”:它们都是基于 S 表达式。如果你因此对它们同样赞美的话,那么你赞美的其实只是 S 表达式,而不是这些语言本身。因为一个语言的本质应该是由它的语义决定的,而跟语法没有很大关系。你甚至可以给同一种语言设计多种不同的语法,而不改变这语言的本质。比如,我曾经给 TeX 设计了 Lisp 的语法,我把它叫做 SchTeX(Scheme + TeX)。SchTeX 的文件看起来是这个样子:

(documentclass article (11pt))
(document
(abstract (...))
(section (First Section)
... )
(section (Second Section)
... )
)

很明显,虽然这看起来像是 Scheme,本质却仍然是 TeX。

所以,因为 Scheme 的语法使用 S 表达式,就把 Scheme 叫做 Lisp 的“方言”,其实是不大准确的做法。Scheme 和 Emacs Lisp,Common Lisp 其实是三种不同的语言。Racket 曾经叫做 PLT Scheme,但是它跟 Scheme 的区别日益增加,以至于现在 PLT 把它改名叫 Racket。这是有他们的道理的。

所以,你也许明白了为什么这篇文章的标题叫做“Lisp 已死,Lisp 万岁!” 因为这句话里面的两个 “Lisp”其实是完全不同的语言。“Lisp 已死”,其实是说 Lisp Machine Lisp 这样的 Lisp,由于严重的设计问题,已经死去。而“Lisp 万岁”,是说像 Scheme,Common Lisp 这样的 Lisp,还会继续存在。它们先进于其它语言的地方,也会更多的被借鉴,被发扬广大。

(其实老 Lisp 的死去还有另外一个重要的原因,那就是因为早期的 Lisp 编译器生成的代码效率非常低下。这个问题我留到下一篇博文再讲。)

【转】Lisp 已死,Lisp 万岁!的更多相关文章

  1. 王垠:Lisp 已死,Lisp 万岁!

    王垠:Lisp 已死,Lisp 万岁!_IT新闻_博客园 王垠:Lisp 已死,Lisp 万岁!

  2. The Lisp Curse /Lisp魔咒

    The Lisp Curse /Lisp魔咒 http://winestockwebdesign.com/Essays/Lisp_Curse.html 英文出处 http://www.soimort. ...

  3. JVM 判断对象已死,实践验证GC回收

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 提升自身价值有多重要? 经过了风风雨雨,看过了男男女女.时间经过的岁月就没有永恒不变 ...

  4. wp已死,metro是罪魁祸首!

    1.这篇文章肯定会有类似这样的评论:“我就是喜欢wp,我就是喜欢metro,我就是软粉“等类似的信仰论者发表的评论. 2.2014年我写过一篇文章,windows phone如何才能在中国翻身? 我现 ...

  5. 泰泽新闻:英特尔三星双否认泰泽Tizen系统已死

    7月8日 据媒体TizenExperts报道,关于“Tizen系统跳票”的传闻已经遭到了英特尔和三星否认. 此前传闻三星自行研制的智能手机Tizen操作系统流产,但如今已经遭到了官方的否认. 英特尔三 ...

  6. Android首席设计师宣称移动概念已死,开发人员应该面向屏幕编写应用而非移动

    腾讯科技对Android首席设计师Duarte"移动已死"訪谈内容的翻译错得离谱,被到处转载,误人视听. 而要真正理解Duarte所想表达的含义,须要深入了解互联网前沿设计理念以及 ...

  7. MVC模式已死

    MVC模式:Model模型 View试图 Control控制器,是目前主流模式,被当作服务器软件入门基本模式学习和掌握,主流框架Struts 1/2 JSF Wicket基本都顺理成章支持MVC模式. ...

  8. JVM学习记录-对象已死吗

    前言 先来回顾一下,在jvm运行时数据区,分为两部分,一个部分是线程共享区,主要包括堆和方法区,另一部是线程私有区分包括本地方法栈,虚拟机栈和程序计数器.在线程私有部分的三个区域是随着线程生和灭的.栈 ...

  9. [转载]致创业者:APP已死 服务永生

    前几日,有位创业者和我讲他在带领团队做一个将爱踢球的人集中在一起的App,我告诉他你的创业方向错了.原因在于你的目的是要为爱踢球的人提供服务,而你现在却在竭尽全力的做App,你应该做的是设计你为爱踢球 ...

随机推荐

  1. 我不是学Java的!我不是学Java的!我不是学Java的!。。。。【自我催眠中】

    我不是学Java的!我不是学Java的!我不是学Java的!....[自我催眠中]

  2. Elasticsearch - 理解字段分析过程(_analyze与_explain)

    我们经常会遇到问题.为什么指定的文档没有被搜索到.许多情况下, 这都归因于映射的定义和分析例程配置存在问题. 针对分析过程的调试,ElasticSearch提供了专用的REST API. _analy ...

  3. Unlicensed ARC session – terminating!

    问题描述 近日,发现ArcGIS10.4中存在很多bug,而且费了好多时间去测试它,最终决定改用10.1.在降级程序时遇到许可问题. 重装ArcGIS10.1后,打开工程,所有引用都自动映射,没报任何 ...

  4. 为什么选择SpringBoot

    JavaEE仅仅使用Spring逐渐变得笨重起来,大量的XML文件存在与项目中,繁琐的配置,整合第三方框架的配置问题,低下的开发效率和部署效率等等问题. Spring Boot解决的问题 (1) Sp ...

  5. openfiler在esxi下的安装配置

    注意分区的时候如果硬盘太小自动分区会导致分配的卷大小不够用 后改为如下: 以root登录: 应该以openfiler登录,口令是password 也可以导入虚拟机安装 升级虚拟机硬件版本 终端登录用户 ...

  6. UBUNTU 字符界面来回切换

    图形界面切换到字符界面: 实体机:Ctrl + Alt + F1 VMware虚拟机:按下ALT+CTRL+SPACE(空格),ALT+CTRL不松开,再按F1.这样就可以切换到字符界面 字符界面切换 ...

  7. http-server使用

    https://segmentfault.com/q/1010000003926981

  8. C# 只能输入字母或数字

    c# 只能输入字母或者数字 或者退格符 方法一:KeyPress private void textBox2_KeyPress(object sender, KeyPressEventArgs e) ...

  9. Oracle Data Integrator 12c----包(Package)

    1 创建"包" Designer->项目->ODI_Exercise ->第一个文件夹->包,右键"新建程序包": "定义&q ...

  10. 转: Orz是一个基于Ogre思想的游戏开发架构

    Orz是一个基于Ogre思想的游戏开发架构,好的结构可以带来更多的功能.Orz和其他的商业以及非商业游戏开发架构不同.Orz更专著于开发者的感受,简化开发者工作.Orz可以用于集成其他Ogre3D之外 ...