页面

2012年6月24日星期日

【外刊IT评论网】坐得越久 死得越快

【外刊IT评论网】坐得越久 死得越快: 又一项研究显示,久坐对你的健康是真的、真的、真的非常有害。请买一个可站着工作的桌子吧! [caption id="attachment_4146" align="aligncenter" width="478" caption="可站着编程的电脑桌"]可站着编程的电脑桌[/caption]
一项对超过20万个澳大利亚人的研究结果给这样一个事实又增加了一份活体证明:坐得越久的人死得越快。研究同时还发现,锻炼不能改变这种趋势——尽管它能有效降低这种风险。
研究结果清晰的告诉我们这样一个简单的信息:多站立、少坐着,这样能延长你的寿命。
尽管那些每周锻炼超过5个小时的人的死亡风险会大大降低,但当他们坐的过久时,这种风险仍然会升高。
目前,“久坐对身体有害”已经被广泛的认可。最近几年的研究表明,在电脑屏幕前、电视前做得太久,或仅是闲坐太久,都会增加你死亡的风险。
这次的调查采取了一种更直接的方式,观察人们每日坐着的时间总和和他们在之后三年内死亡率之间的关系,希望能给久坐的危害程度标个数字。
结果让人震惊,每天坐着超过11小时的人在未来三年的死亡风险要比每天坐着少于4小时的人的死亡风险高出40%。这是经过了对年龄、体重、物理锻炼、健康水平等所有会影响到死亡风险的因素进行校正后得到的结果。同时得到的一个正比数据是:坐得越久,死亡风险越高。
这个研究是萨克斯研究所(Sax Institute)的45 and Up研究项目的组成部分。45 and Up Study是南半球目前最大的真正进行的关于健康衰老研究项目。研究数据来自222497个超过45岁的澳大利亚人每天自主报告的总计坐的时间。研究者拿这些数据跟他们在之后三年的死亡率进行了对比。
不管他们健康还是有病,喜欢运动还是不爱运动,他们坐得越久,在未来三年里的死亡风险就会越高。锻炼可以大量的降低这种风险:坐的最久的人比坐的很少的人的死亡风险只高出40%,但拿坐的最久且锻炼最少的人和坐的很少但锻炼最多的对比,这个数字会变成100%。尽管每周锻炼超过5小时的人的风险会低很多,但当他们做的太久时,风险度仍是往上走的。
换句话说就是,你需要去锻炼,但同等重要的事是,尽量少坐。
有一篇社论曾建议说,证据已经如此丰富,我们的大夫完全应该在给病人的处方中建议他们减少坐着的时间。但我们自己为什么不能主动行动,给自己开出这样的药方呢。
据粗略统计,人们在休闲时90%以上的时间是坐着的。所以,我们还有很大改善的空间。

本文来自外刊IT评论网(www.aqee.net),原始地址:坐得越久 死得越快



2012年6月18日星期一

初学者程序语言的选择

 
 

小小才子 通过 Google 阅读器发送给您的内容:

 
 

于 12-5-24 通过 当然我在扯淡 作者:王垠

很多人都关心这个问题,来信问我。我一直想总结一下经验,今天终于开始写这篇文章。我的文章一般都会在发表之后有所改动,所以如果转载请只给出链接,以便得到最新的版本。


面向对象语言不适合入门

有的人抱怨很多学校开始教授 Java 而不是以前的 C 或者 Pascal。的确,Java 有很多问题,使得它不适合作为一种入门语言。其实 Java 本质上是把自身的一种古板的设计强加于程序员,使得他们失去了灵活的思维。比如 Java 缺少高阶函数,也就是不能把函数作为参数或者变量传递,这导致了需要使用繁琐的设计模式 (design patterns) 来达到甚至对于 C 语言都直接了当的事情。

Java 在教学中的过度使用,已经开始引起对整个业界的负面作用。很多公司里的程序员喜欢生搬硬套一些不必要的设计模式,其实什么好事情也没干,只是使得程序冗长难懂。我从来不用通常所谓的设计模式,即使我需要实现一些模式比如 visitor pattern 的功能,我也是用的自己的设计。你完全可以在 Java 语言里达到 visitor 的实际效果,却不使用通常所谓的 visitor。这个我以后可能详细说一下。但是我的方法虽然比普通的办法好,也只能算是一个变通方案 (workaround)。在这里,我只是要说,Java 不适合初学者,因为初学者不应该学习变通方案。这些变通并不是在所有语言里都需要的。教会他们这些东西,只会让他们对错误的思想记忆更加深刻,从而在将来的设计中犯同样的错误,导致恶性循环。比如我在某些人写的 Lisp 程序里面居然看到 visitor pattern,真是哭笑不得。本来是因为一个语言有毛病所以我们使用变通方案,但是这种变通居然被用到了本来没有这毛病的语言身上!

那么除了 Java,是否可以考虑具备高阶函数的 Python,JavaScript,Ruby,Scala,Clojure 这些语言呢?我不否认它们的实用价值,但是作为初学编程的人,它们不是很好的选择。因为这些语言的设计者,并不是最好的程序语言专家,他们有时候甚至不明白一些最基本的概念。以至于这些语言里面的很多东西,虽然起着响亮的名字,却并没有"地道"的实现。比如 Python 也有 lambda,但是 Python 的 lambda 是一个被阉割的 lambda,它并没有 lambda 完整的功能,以至于它只能被用于最简单的地方。这就是为什么 Guido van Rossum 曾经扬言要把 lambda 从 Python3 里去掉,因为他自己都不懂得 lambda 是什么。Python 还有别的别的一些明显糟糕的设计,却直到 Python3 才部分的被 Guido 觉悟到。我实现过 Python 的类型推导,所以我知道这些设计的幼稚可笑。不要小看这个类型推导,它是基于一个先进的概念叫"抽象解释"(abstract interpretation),为此我基本上实现了整个 Python 的语义。在摸索 Python 语义的过程中,我很惊讶的发现,有时候我会在某些语义概念上"不小心"做出正确的选择,而到头来我却需要把它们改成错误的,否则我无法准确的符合 Python 的语义。其它几个语言也有类似的问题,但是恐怕只有当你领会到函数式语言的真谛之后才会明白。比如,精通 Haskell 的人都会发现用 Scala 其实非常折腾。

我多次的想写一篇关于面向对象语言的毛病的文章,到头来却发现很难下笔。这就像听惯了巴赫的音乐之后,再让你去评论流行音乐有什么毛病。你根本说不清楚,因为没有体会过真正的杰作的人,他们不会理解你说的任何理由。你只能轻描淡写的让他们去尝试,去体验。到头来如果他们体会到了,听到流行音乐就会恍然大悟的发现,它们原来是如此的苍白无趣,味同嚼蜡!但是如果他们不能体会到,那也只能由他们去了。


底层语言不适合入门

那么是否 C 就会好一些呢?其实也不是。很多人推崇 C,因为它可以让人接近"底层",也就是接近机器的表示,这样就意味着它速度很快。这里其实有三个问题:

  1. 接近"底层"是否对于初学者是好事?
  2. "速度快的语言"是什么意思?
  3. 接近底层的语言是否一定速度快?

对于第一个问题,我的答案是否定的。其实编程最重要的思想是高层的语义(semantics)。语义构成了人关心的问题以及解决它们的算法。而具体的实现(implementation)比如一个整数用几个字节表示,虽然还是重要,但却不是至关重要的。如果把实现作为学习的主要目标,就本末倒置了。因为实现是可以改变的,而它们所表达的本质却不会变。所以很多人发现自己学会的东西,过不了多久就"过时"了。那就是因为他们学习的不是本质,而只是具体的实现。

其次,谈语言的"速度",其实是一句空话。语言只负责描述一个程序,而程序运行的速度,其实绝大部分不取决于语言。它主要取决于 1)算法 和 2)编译器的质量。编译器和语言基本是两码事。同一个语言可以有很多不同的编译器实现,每个编译器生成的代码质量都可能不同,所以你没法说"A 语言比 B 语言快"。你只能说"A 语言的 X 编译器生成的代码,比 B 语言的 Y 编译器生成的代码高效"。这几乎等于什么也没说,因为 B 语言可能会有别的编译器,使得它生成更快的代码。

我举个例子吧。在历史上,Lisp 语言享有"龟速"的美名。有人说"Lisp 程序员知道每个东西的值,却不知道任何事情的代价",讲的就是这个事情。但这已经是很久远的事情了,现代的 Lisp 系统能编译出非常高效的代码。比如商业的 Chez Scheme 编译器,能在5秒钟之内编译它自己,编译生成的目标代码非常高效。它的实现真的令人惊叹,因为它的作者 R. Kent Dybvig 几乎不依赖于任何已有的软件和设计。这个编译器从最初的 parser,到宏扩展,语义分析,寄存器分配,各种优化,…… 一直到汇编器,函数库,全都是他一个人写的。它可以直接把 Scheme 程序编译到多种处理器的机器指令,而不通过任何第三方软件。它内部的一些算法,其实比开源的 LLVM 之类的先进很多。但是由于是商业软件,这些算法一直被作为机密没有发表。

另外一些函数式语言也能生成高效的代码,比如 OCaml。在一次程序语言暑期班上,Cornell 的 Robert Constable 教授讲了一个故事,说是他们用 OCaml 重新实现了一个系统,结果发现 OCaml 的实现比原来的 C 语言实现快了 50 倍。经过 C 语言的那个小组对算法多次的优化,OCaml 的版本还是快好几倍。这里的原因其实在于两方面。第一是因为函数式语言把程序员从底层细节中解脱出来,让他们能够迅速的实现和修改自己的想法,所以他们能够迅速的找到更好的算法。第二是因为 OCaml 有高效的编译器实现,使得它能生成很好的代码。

从上面的例子,你也许已经可以看出,其实接近底层的语言不一定速度就快。因为编译器这种东西其实可以有很高级的智能,甚至可以超越任何人能做到的底层优化。但是编译器还没有发展到可以代替人来制造算法的地步(虽然有些技术,比如 supercompilation,有可能自动生成新的算法)。所以现在人需要做的,其实只是设计和优化自己的高层算法。


学习 1.5 种函数式语言

那么我推荐什么样的语言呢?虽然我不是函数式语言的狂热分子,但是我觉得相对而言,函数式的语言相对来说更适合入门者。因为它们不但让人专注于算法和对问题的解决,而且没有面向对象语言那些思维的限制。那么现在的问题是,哪一种函数式语言。这是一个很难回答的问题,因为没有一种函数式语言拥有所有的优点,而它们的狂热分子们经常把缺点也说成是优点,结果你还是会被误导。所以我觉得,初学者最好学习两种函数式语言。不要被我吓倒了,你并不需要学习这些语言的所有细枝末节,而只需要学习最精华的部分。所有剩余的细节,会在实际使用中很容易的被填补上。因为你没必要学习它们的全部,所以我称之为"1.5种"函数式语言。我后面会提一下哪些是精华的,哪些是最开头没必要学的。

其实真正严格意义上的函数式语言不多。最可信而又广泛使用的的函数式语言是 Scheme, Haskell, OCaml,SML, Clean 等。就我的观点,首先可以从 Scheme 入门,然后学习一些 Haskell (但不是全部),之后其它的也就触类旁通了。我不推荐 OCaml 和 SML,因为它们的类型系统里面有很多不成熟的设计,导致你需要记住太多不必要的东西。

从 Scheme(而不是 Haskell)作为入门的第一步,是因为:

  1. Scheme 没有像 Haskell 那样的静态类型系统 (static type system)。并不是说静态类型不好,但是我不得不说,Haskell 那样的静态类型系统,还远远没有发展到可以让人可以完全的写出符合事物本质的程序来。比如,一些重要的概念比如 Y combinator,没法用 Haskell 直接写出来。当然你可以在 Haskell 里面使用作用类似 Y combinator 的东西(比如 fix,或者利用它的 laziness),但是这些并不揭示递归的本质,你只是在依靠 Haskell 已经实现的递归来进行递归,而不能实际的体会到递归是如何产生的。而用 Scheme,你可以轻松的写出 Y combinator,并且实际的投入使用。
  2. Scheme 不需要 monad。Haskell是一个"纯函数式" (purely functional) 的语言,所有的"副作用"(side-effect),比如打印字符到屏幕,都得用一种深奥而偏僻的概念叫 monad 实现。这种概念其实并不是本质的,它所有的功能都可以通过"状态传递" (state passing) 来实现。通过写状态传递程序,你可以清楚的看到 monad 的本质。可以说 monad 是 Haskell 的一个"设计模式"。过早的知道这个东西,并不有助于理解函数式程序设计的本质。

那么为什么又要学 Haskell?那是因为 Haskell 含有 Scheme 缺少的一些东西,并且没有 Scheme 设计上的一些问题。比如:

  1. 缺少模式匹配:Scheme 没有一个标准的,自然的模式匹配 (pattern matching) 系统,而 Haskell 的模式匹配是一个优美的实现。
  2. 类型模糊:比如 Scheme 把所有不是 #f (false)的值都作为 true,这是不对的。Haskell 里面的 Boolean 就只有两个值:True 和 False。Scheme 程序员声称这样可以写出简洁的代码,因为 (or x y z) 可以返回一个具体的,而不只是一个布尔变量。但是就为了在少数情况下可以写出短一点的代码,是否值得付出如此沉痛的代价?我看到这个设计带来了很多无需有的问题。
  3. 宏系统:宏 (macro) 通常被认为是 Lisp 系列语言的一个重要优点。但是我要指出的是,它们并不是必要的,至少对于初学者是这样。其实如果一个语言的语义设计好了,你会几乎不需要宏。因为宏的本质是让程序员可以自己修改语言的设计,添加新的构造。可是宏的主要缺点是,它把改变语言这种极其危险的"权力"给人滥用了。其实只有极少数的人具有改变一个语言所需的智慧和经验。如果让普通程序员都能使用宏,那么程序将变得非常难以理解。所以最开头其实不需要学习宏的使用,也不必为略过这个东西而产生负罪感。等你进步到可以设计自己的程序语言,你自然会明白宏是什么东西。

(注意,这些是我自己的观点,并不代表 Scheme 设计者们的观点。)


推荐的书籍

The Little Schemer我觉得 Dan Friedman 的 The Little Schemer (TLS) 是最好,最精华的编程入门教材。它的前身叫《The Little Lisper》。很多资深的程序语言专家都是从这本书学会了 Lisp。虽然它叫 "The Little Schemer",但它并不使用 Scheme 所有的功能,而是忽略了上面提到的 Scheme 的毛病,直接进入最关键的主题:递归和它的基本原则。在第九章,你会学到如何从无到有推导出 Y combinator。我做过一个幻灯片,演示的就是这里的推导过程。这本书不但很薄,很精辟,而且相对于其他编程书籍非常便宜(在美国才卖 $23)。

SICPThe Little Schemer 其实是比较难的读物,所以我建议把它作为下一步精通的读物。Structure and Interpretation of Computer Programs 比较适合作为第一本教材。但是我需要提醒的是,你最多只需要看完第三章。因为从第四章开始,作者开始实现一个 Scheme 解释器,但是作者的实现并不是最好的方式。你可以从别的地方更好的学到这些东西。具体在哪里学,我还没想好(也许我自己写个教学也说不定)。不过也许你可以看完 SICP 第一章之后就可以开始看 TLS

A Gentle Introduction to Haskell对于 Haskell,我最开头看的是 A Gentle Introduction to Haskell,因为它特别短小。当时我已经会了 Scheme,所以不需要再学习基本的函数式语言的东西。我从这个文档学到的只不过是 Haskell 对于类型和模式匹配的概念。Real World Haskell 是一本流行的教材,但是它试图包罗万象,所以很多地方过于冗长。最根本的函数式编程概念,还是 TLS 讲的透彻。


过度到面向对象语言

那么如果从函数式语言入门,如何过渡到面向对象语言呢?毕竟大部分的公司用的是面向对象语言。如果你真的学会了函数式语言,你真的会发现面向对象语言已经易如反掌。函数式语言的设计比面向对象语言简单和强大很多,而且几乎所有的函数式语言教材(比如 SICP)都会教你如何实现一个面向对象系统。你会深刻的看到面向对象的本质以及它存在的问题,所以你会很容易的搞清楚怎么写面向对象的程序,并且会发现一些窍门来避开它们的局限。你会发现,即使在实际的工作中必须使用面向对象语言,也可以避免面向对象的思维方式,因为面向对象的思想带来的大部分是混乱和冗余。


深入底层

那么是不是完全不需要学习底层呢?当然不是。但是一开头就学习底层硬件,就会被纷繁复杂的硬件设计蒙蔽头脑,看不清楚本质上简单的原理。

在学会高层的语言之后,可以进行语义学编译原理的学习。简言之,语义学 (semantics) 就是研究程序的符号表示如何对机器产生"意义",通常语义学的学习包含 lambda calculus 和各种解释器的实现。编译原理 (compilation) 就是研究如何把高级语言翻译成低级的机器指令。编译原理其实包含了计算机的组成原理,比如二进制的构造和算术,处理器的结构,内存寻址等等。但是结合了语义学和编译原理来学习这些东西,会事半功倍。因为你会直观的看到为什么现在的计算机系统会设计成这个样子:为什么处理器里面有寄存器(register),为什么需要堆栈(stack),为什么需要堆(heap),它们的本质是什么。这些甚至是很多硬件设计者都不明白的问题,所以它们的硬件里经常含有一些没必要的东西。因为他们不理解语义,所以经常不明白他们的硬件到底需要哪些部件和指令。但是从高层语义来解释它们,就会揭示出它们的本质,从而可以让你明白如何设计出更加优雅和高效的硬件。

这就是为什么一些程序语言专家后来也开始设计硬件。比如 Haskell 的创始人之一 Lennart Augustsson,后来设计了 BlueSpec,一种高级的硬件描述语言,可以 100% 的合成 (synthesis) 为硬件电路。Scheme 也被广泛的使用在硬件设计中,比如 Motorola 和 Cisco,它们都是 Chez Scheme 的用户。


这基本上就是我对程序语言入门的建议。我可能还会修改其中一些内容。有问题的话欢迎发邮件到我的信箱:shredderyin@gmail.com。谢谢大家。


  青春就应该这样绽放  游戏测试:三国时期谁是你最好的兄弟!!  你不得不信的星座秘密

 
 

可从此处完成的操作:

 
 

Dan Friedman 的故事 (3)——miniCoq

 
 

小小才子 通过 Google 阅读器发送给您的内容:

 
 

于 12-6-11 通过 当然我在扯淡 作者:王垠

你永远想象不到 Dan Friedman 的思想的极限在哪里。当你认为他是一个函数式语言专家的时候,他发明了 miniKanren,一种逻辑式编程语言 (logic programming language),并且写出 《The Reasoned Schemer》,用于教授逻辑编程。当你认为他不懂类型系统的时候,他开始捣鼓最尖端的 Martin-Löf 类型理论,并且开始设计机器证明系统。而他做这些,完全是出于自己的兴趣。他从来不在乎别人在这个方向已经做到了什么程度,却经常能出乎意料的简化别人的设计。

有一次系里举办教授们的"闪电式演讲"(lightening talk),每位教授只有5分钟时间上去介绍自己的研究。轮到 Friedman 的时候,他慢条斯理的走上去,说:"我不着急。我只有几句话要说。我不知道我能不能拖够5分钟……"大家都笑了。他接着说:"我现在最喜欢的东西是 Curry-Howard correspondence 和定理证明。我觉得现在的机器证明系统太复杂了,比如 Coq 有 nnnnn 行代码。我想在 x 年之内,简化 Coq,得到一个 miniCoq……"

miniCoq... 听到这个词全场都笑翻了。为什么呢?自己去联想,往歪处想  从此,"Dan Friedman 的 miniCoq" 成为了 IU 的程序语言学生茶余饭后的笑话。

但是他没有吹牛,他总是说到做到。他已经写出一个简单的定理证明工具叫 JBob(迫于社会舆论压力,不能叫 miniCoq),而且正在写一本书叫 《The Little Prover》,用来教授最重要的定理证明思想。他开始在 C311 上给本科生教授这些内容。我看了那本书的初稿,获益至深,那是很多 Coq 的教材都不涉及的最精华的道理。它不仅教会我如何使用定理证明系统,而且教会了我如何设计一个定理证明系统。我对他说:"你总是有新的东西教给我们。每隔两年,我们就得重新上一次你的课!"

  青春就应该这样绽放  游戏测试:三国时期谁是你最好的兄弟!!  你不得不信的星座秘密

 
 

可从此处完成的操作:

 
 

2012年6月3日星期日

Lisp的永恒之道

Lisp的永恒之道:
感谢 Todd投递本文 – 微博帐号:weidagang

Lisp之魅

长久以来,Lisp一直被许多人视为史上最非凡的编程语言。它不仅在50多年前诞生的时候带来了诸多革命性的创新并极大地影响了后来编程语言的发展,即使在一大批现代语言不断涌现的今天,Lisp的诸多特性仍然未被超越。当各式各样的编程语言摆在面前,我们可以从运行效率、学习曲线、社区活跃度、厂商支持等多种不同的角度进行评判和选择,但我特别看中的一点在于语言能否有效地表达编程者的设计思想。学习C意味着学习如何用过程来表达设计思想,学习Java意味着学习如何用对象来表达设计思想,而虽然Lisp与函数式编程有很大的关系,但学习Lisp绝不仅仅是学习如何用函数表达设计思想。实际上,函数式编程并非Lisp的本质,在已经掌握了lambda、高阶函数、闭包、惰性求值等函数式编程概念之后,学习Lisp仍然大大加深了我对编程的理解。学习Lisp所收获的是如何“自由地”表达你的思想,这正是Lisp最大的魅力所在,也是这门古老的语言仍然具有很强的生命力的根本原因。

Lisp之源

Lisp意为表处理(List Processing),源自设计者John McCarthy于1960年发表的一篇论文《符号表达式的递归函数及其机器计算》。McCarthy在这篇论文中向我们展示了用一种简单的数据结构S表达式(S-expression)来表示代码和数据,并在此基础上构建一种完整的语言。Lisp语言形式简单、内涵深刻,Paul Graham在《Lisp之根源》中将其对编程的贡献与欧几里德对几何的贡献相提并论。

Lisp之形

然而,与数学世界中简单易懂的欧氏几何形成鲜明对比,程序世界中的Lisp却一直是一种古老而又神秘的存在,真正理解其精妙的人还是少数。从表面上看,Lisp最明显的特征是它“古怪”的S表达式语法。S表达式是一个原子(atom),或者若干S表达式组成的列表(list),表达式之间用空格分开,放入一对括号中。“列表“这个术语可能会容易让人联想到数据结构中的链表之类的线形结构,实际上,Lisp的列表是一种可嵌套的树形结构。下面是一些S表达式的例子:
foo

()

(a b (c d) e)

(+ (* 2 3) 5)

(defun factorial (N)
    (if (= N 1)
        1
        (* N (factorial (- N 1)))
    )
)
据说,这个古怪的S表达式是McCarthy在发明Lisp时候所采用的一种临时语法,他实际上是准备为Lisp加上一种被称为M表达式(M-expression)的语法,然后再把M表达式编译为S表达式。用一个通俗的类比,S表达式相当于是JVM的字节码,而M表达式相当于Java语言,但是后来Lisp的使用者都熟悉并喜欢上了直接用S表达式编写程序,并且他们发现S表达式有许多独特的优点,所以M表达式的引入也就被无限期延迟了。
许多Lisp的入门文章都比较强调Lisp的函数式特性,而我认为这是一种误导。真正的Lisp之门不在函数式编程,而在S表达式本身,Lisp最大的奥秘就藏在S表达式后面。S表达式是Lisp的语法基础,语法是语义的载体,形式是实质的寄托。“S表达式”是程序的一种形,正如“七言”是诗的一种形,“微博”是信息的一种形。正是形的不同,让微博与博客有了质的差异,同样的道理,正是S表达式让Lisp与C、Java、SQL等语言有了天壤之别。

Lisp之道

一门语言能否有效地表达编程者的设计思想取决于其抽象机制的语义表达能力。根据抽象机制的不同,语言的抽象机制形成了面向过程、面向对象、函数式、并发式等不同的范式。当你采用某一种语言,基本上就表示你已经“面向XXX“了,你的思维方式和解决问题的手段就会依赖于语言所提供的抽象方式。比如,采用Java语言通常意味着采用面向对象分析设计;采用Erlang通常意味着按Actor模型对并发任务进行建模。
有经验的程序员都知道,无论是面向XXX编程,程序设计都有一条“抽象原则“:What与How解耦。但是,普通语言的问题就在于表达What的手段非常有限,无非是过程、类、接口、函数等几种方式,而诸多领域问题是无法直接抽象为函数或接口的。比如,你完全可以在C语言中定义若干函数来做到make file所做的事情,但C代码很难像make file那样声明式地体现出target、depends等语义,它们只会作为实现细节被淹没在一个个的C函数之中。采用OOP或是FP等其它范式也会遇到同样的困难,也就是说make file语言所代表的抽象维度与面向过程、OOP以及FP的抽象维度是正交的,使得各种范式无法直接表达出make file的语义。这就是普通语言的“刚性”特征,它要求我们必须以语言的抽象维度去分析和解决问题,把问题映射到语言的基本语法和语义。
更进一步,如果仔细探究这种刚性的根源,我们会发现正是由于普通语言语法和语义的紧耦合造成了这种刚性。比如,C语言中printf(“hello %s”, name)符合函数调用语法,它表达了函数调用语义,除此之外别无他义;Java中interface IRunnable { … }符合接口定义语法,它表达了接口定义语义,除此之外别无他义。如果你认为“语法和语义紧耦合“是理所当然的,看不出这有什么问题,那么理解Lisp就会让你对此产生更深的认识。
当你看到Lisp的(f a (b c))的时候,你会想到什么?会不会马上联想到函数求值或是宏扩展?就像在C语言里看到gcd(10, 15)马上想到函数调用,或者在Java里看到class A马上想到类定义一样。如果真是这样,那它就是你理解Lisp的一道障碍,因为你已经习惯了顺着语言去思考,总是在想这一句话机器怎么解释执行?那一句话又对应语言的哪个特性?理解Lisp要反过来,让语言顺着你,Lisp的(f a (b c))可以是任何语义,完全由你来定,它可以是函数定义、类定义、数据库查询、文件依赖关系,异步任务的执行关系,业务规则 …
下面我准备先通过几个具体的例子逐步展示Lisp的本质。需要说明的是,由于Lisp的S表达式和XML的语法形式都是一种树形结构,在语义表达方面二者并无本质的差别。所以,为了理解方便,下面我暂且用多数人更为熟悉的XML来写代码,请记住我们可以很轻易地把XML代码和Lisp代码相互转换。
首先,我们可以轻易地用XML来定义一个求两个数最大公约数的函数:
<func name='gcd' return_type='int'>
        <params>
            <a type='int'/>
            <b type='int'/>
        </params>
        <body>
            <if>
               <equals>
                   <a/>
                   <int>0</int>
               </equals>
            </if>
            <then>
                <return><b/></return>
            </then>
            <else>
                <return>
                    <gcd>
                        <modulo><b/><a/></modulo>
                        <a/>
                    </gcd>
                </return>
            </else>
        </body>
    </func>
其次,我们可以用它来定义类:
<class name="Computer">
        <field access="private" type="MainBoard" name="main-board" />
        <field access="private" type="CPU" name="cpu" />
        <field access="private" type="Memory" name="memory" />

        <method access="public" return_type="boolean" name="powerOn" />
            <params>...</params>
            <body>...</body>
        </method>

        <method access="public" return_type="boolean" name="powerOff" />
            <params>...</params>
            <body>...</body>
        </method>
    </class>
还可以轻易地用它来编写关系查询:
<sql>
    <select>
        <column name="employees.id" />
        <column name="bonus.amount" />
    </select>
    <from>
        <table name="employees" />
        <table name="bonus" />
    </from>
    <where>
        <equals>
            <column name="employees.id" />
            <column name="bonus.employee_id" />
        </equals>
    </where>
</sql>
还可以用它来实现类似make file的自动化构建(语法取自ant):
<project name="MyProject" default="dist" basedir=".">
        <property name="src" location="src"/>
        <property name="build" location="build"/>
        <property name="dist"  location="dist"/>

        <target name="init">
            <mkdir dir="${build}"/>
        </target>

        <target name="compile" depends="init" description="compile the source " >
            <javac srcdir="${src}" destdir="${build}"/>
        </target>

        <target name="dist" depends="compile" description="generate the distribution" >
            <mkdir dir="${dist}/lib"/>
            <jar jarfile="${dist}/lib/MyProject-${DSTAMP}.jar" basedir="${build}"/>
        </target>

        <target name="clean" description="clean up" >
            <delete dir="${build}"/>
            <delete dir="${dist}"/>
        </target>
    </project>
一口气举了这么多个例子,目的在于用XML这种树形结构来说明Lisp的S表达式所能够描述的语义。不知道你是否发现了S表达式和XML这种树形语法在语义构造方面有着特别的“柔性”?我们可以轻易地用它构造出函数、变量、条件判断语义;类、属性、方法语义;可以轻易地构造出关系模型的select、where语义;可以轻易地构造出make的target、depends语义,等等数不清的语义。在普通语言里,你可以定义一个函数、一个类,但你无法为C语言增加匿名函数特性,也没法给Java语言加上RAII语义,甚至连自己创造一个foreach循环都不行,而自定义语义意味着在Lisp之上你创造了一门语言!不管是面向过程,面向对象,函数式,还是关系模型,在Lisp里统统都变成了一种DSL,而Lisp本身也就成了一种定义语言的语言,即元语言(Meta Language)。
Lisp的柔性与S表达式有着密切的关系。Lisp并不限制你用S表达式来表达什么语义,同样的S表达式语法可以表达各种不同领域的语义,这就是语法和语义解耦。如果说普通语言的刚性源于“语法和语义紧耦合”,那么Lisp的柔性正是源于“语法和语义解耦”!“语法和语义解耦”使得Lisp可以随意地构造各种领域的DSL,而不强制用某一种范式或是领域视角去分析和解决问题。本质上,Lisp编程是一种超越了普通编程范式的范式,这就是Lisp之道:面向语言编程(LOP, Language Oriented Programming)。Wikipedia上是这样描述LOP的:
Language oriented programming (LOP) is a style of computer programming in which, rather than solving problems in general-purpose programming languages, the programmer creates one or more domain-specific languages for the problem first, and solves the problem in those languages … The concept of Language Oriented Programming takes the approach to capture requirements in the user’s terms, and then to try to create an implementation language as isomorphic as possible to the user’s descriptions, so that the mapping between requirements and implementation is as direct as possible.
LOP范式的基本思想是从问题出发,先创建一门描述领域模型的DSL,再用DSL去解决问题,它具有高度的声明性和抽象性。SQL、make file、CSS等DSL都可以被认为是LOP的具体实例,下面我们再通过两个常见的例子来理解LOP的优势。
例1:在股票交易系统中,交易协议定义若干二进制的消息格式,交易所和客户端需要对消息进行编码和解码。
消息格式是一种抽象的规范,本身不对语言做任何的限制,你可以用C,C++,Java,或者Python。普通的实现方式是按照消息格式规范,在相应的语言中定义消息结构,并编写相应的编解码函数。假设为一个消息定义结构和实现编解码函数的工作量为M,不同消息类型的数量为N,这种方式的工作量大致为M*N。也就是说每增加一种消息类型,就需要为该消息定义结构,实现编解码函数,引入bug的可能性当然也和M*N成正比。如果仔细观察不难发现,各个消息结构其实是高度类似的,编解码函数也大同小异,但是普通语言却找不到一种抽象机制能表达这种共性,比如,我们无法通过面向对象的方法定义一个基类把消息结构的共性抽象出来,然后让具体的消息去继承它,达到复用的目的。这正是由于普通语言的抽象维度限制所致,在普通语言中,你只能从函数、接口等维度对事物进行抽象,而恰好消息格式共性所在的维度与这些抽象维度并不匹配。
其实,不同消息类型的共性在于它们都具有相同的领域语义,比如:“某字段内容是另一个字段内容的md5码”就是一种消息格式的领域语义,这种领域语义是OOP的抽象机制无法描述的。LOP的思路是先创建一门消息定义DSL,比如,类似Google的Protocol Buffer,Android的AIDL。然后,通过DSL编写消息定义文件,直接声明式地描述消息的结构特征,比如,我们可以声明式地描述“某字段内容是另一个字段内容的md5码”。我们还需要为DSL开发编译器用于生成C、Java等通用语言的消息定义和编解码函数。
有了消息定义DSL和编译器之后,由于DSL编写消息定义是一种高度声明式的编程方法,每增加一种消息的只需要多编写一个消息定义文件而已,工作量几乎可以忽略不计。所有的工作量都集中在编译器的开发上,工作量是一个常数C,与消息的数量没有关系;质量保证方面也只需要关注编译器这一点,不会因为增加新的消息类型而引入bug。
例2:在图书管理系统中,需要支持在管理界面上对书籍、学生、班级等各种实体进行管理操作。
如果按传统的三层架构,一般需要在后端程序中为每一种实体定义一个类,并定义相应的方法实现CRUD操作,与之相应的,还需要在前端页面中为每一个实体编写相应的管理页面。这些实体类的CRUD操作都是大同小异的,但细节又各不相同,虽然我们很想复用某些共同的设计实现,但OOP所提供的封装、继承、多态等抽象机制不足以有效捕获实体之间的共性,大量的代码还是必须放在子类中来完成。比如,Student和Book实体类的实现非常相似,但是如果要通过OOP的方式去抽象它们的共性,得出的结果多半是Entity这样的大而空的基类,很难起到复用的效果。
其实,不同实体之间的共性还是在于它们具有相同的领域语义,比如:实体具有属性,属性具有类型,属性具有取值范围,属性具有可读取、可编辑等访问属性,实体之间有关联关系等。LOP方法正是直接面向这种领域语义的。采用LOP方法,我们并不需要为每一个实体类单独编写CRUD方法,也不需要单独编写管理页面,只需要定义一种DSL并实现其编译器;然后,用DSL声明式地编写实体描述文件,去描述实体的属性列表,属性的类型、取值范围,属性所支持的操作,属性之间的关系和约束条件等;最后,通过这个实体描述文件自动生成后端的实体类和前端管理页面。采用LOP,不论前后端采用何种技术,Java也好,C#也好,JSP也好,ASP.NET也好,都可以自动生成它们的代码。采用LOP的工作量和质量都集中在DSL的设计和编译器的开发,与实体的数量无关,也就是说,越是庞大的系统,实体类越多越是能体现LOP的优势。
通过上面两个小例子我们可以感受到,LOP是一种面向领域的,高度声明式的编程方式,它的抽象维度与领域模型的维度完全一致。LOP能让程序员从复杂的实现细节中解脱出来,把关注点集中在问题的本质上,从而提高编程的效率和质量。
接下来的问题是如果需要为某领域设计DSL,我们是应该发明一门类似SQL这样的专用DSL呢,还是用XML或S表达式去定义DSL呢?它们各有何优缺点呢?
我认为采用XML或S表达式定义DSL的优点主要有:1) SQL、make file、CSS等专用DSL都只能面向各自的领域,而一个实际的领域问题通常是跨越多个领域的,有时我们需要将不同领域融合在一起,但是由于普通语言的刚性,多语言融合通常会是一件非常困难的事情,而XML和S表达式语法结构的单一性和“代码及数据”的特点使得跨领域融合毫无障碍。2) 在为DSL开发编译器或解释器的方面,二者难度不同。对XML和S表达式定义的DSL进行语法分析非常简单,相比之下,对SQL这样的专用DSL进行语法分析,虽然可以借助Lex、Yacc、ANTLR等代码生成工具,但总的来讲复杂度还是要明显高一些。
当然,XML和S表达式的优点也正好是其缺点,由于XML和S表达式的语法形式是固定的,不能像专用DSL那样自由地设计语法。所以,一般来讲专用DSL的语法显得更加简洁。换句话说,XML和Lisp其实是在语法和语义间做了一个交换,用语法的限制换来了语义的灵活。

Lisp之器

接下来我们继续探讨DSL的解释执行问题。DSL代码的解释执行一般分为3种典型的方式:1) 通过专门的解释器解释执行;2) 编译生成其他语言的代码,再通过其他语言的解释器解释执行(或编译运行);3) 自解释。比如,第1类的代表是SQL,上一节举的两个例子都属于第2类,而第3类自解释正是Lisp的特色。
为了理解自解释,我们可以先从内部DSL的解释执行说起。内部DSL是指嵌入在宿主语言中的DSL,比如,Google Test单元测试框架定义了一套基于流畅接口(Fluent Interface)的C++单元测试DSL。从语义构造的角度看,内部DSL直接借用宿主语言的语法定义了自己的领域语义,是一种语法和语义解耦;从解释执行的角度看,内部DSL是随宿主语言的解释器而自动解释的,不需要像外部DSL一样开发专门的解释器,因而实现的代价很低。当然,并不是说设计内部DSL不用关心任何的解释实现,实际上,还是需要熟悉宿主语言的特性,并利用该特性使得DSL能随着宿主语言的解释器得到解释执行。
Lisp拥有强大的自解释特性,这得益于独一无二的Lisp之器:宏 (macro)。宏使得Lisp编写的DSL可以被Lisp解释器直接解释执行,这在原理上与内部DSL是相通的,只是内部DSL一般是利用宿主语言的链式调用等特性,通常形式简陋,功能有限,而Lisp的宏则要强大和灵活得多。
C语言中也有宏的概念,不过Lisp的宏与C语言的宏完全不同,C语言的宏是简单的字符串替换。比如,下面的宏定义:
#define square(x) (x*x)
square(1+1)的期望结果是4,而实际上它会被替换成(1+1*1+1),结果是3。这个例子说明,C语言的宏只在预编译阶段进行简单的字符串替换,对程序语法结构缺乏理解,非常脆弱。Lisp的宏不是简单的字符串替换,而是一套完整的代码生成系统,它是在语法解析的基础上把Lisp代码从一种形式转换为另一种形式,本质上起到了普通语言编译器的作用。不同的是,普通编译器是把一种语言的代码转换为另一种语言的代码,比如,Java编译器把Java代码转换成Java字节码;而Lisp宏的输入和输出都是S表达式,它本质上是把一种DSL转换为另一种DSL。下面的例子是宏的一个典型用法。
例3:假设Lisp解释器已经具备解释执行面向过程DSL的能力,需要实现类似ant的自动化构建工具。
我们可以基于宏构建一门类ant的DSL,宏的作用是把类ant DSL通过宏展开变成面向过程的DSL,最后被Lisp解释器所解释执行。这样用Lisp编写的ant DSL就不需要被编译为其他语言,也不需要像XML的ant一样依赖于专门的解释器了。
当然,和开发专门的解释器/编译器相比,Lisp的宏也并非没有缺点,宏难以理解,开发和调试更加困难。到底是开发专门的解释器/编译器还是直接采用宏应该视具体情况而定。

总结

Lisp采用单一的S表达式语法表达不同的语义,实现了语法和语义解耦。这使得Lisp具有强大的语义构造能力,擅长于构造DSL实现面向语言编程,而宏使得Lisp具有自解释能力,让不同DSL之间的转换游刃有余。进入Lisp的世界应当从理解面向语言编程入门,这是Lisp之道,而函数式编程和宏皆为Lisp之器,以道驭器方为正途。

后记

本文是我学习Lisp的一个总结,也是写给有兴趣学习Lisp的程序员的入门资料。必须说明,我还是一个标准的Lisp初学者,几乎没有写过像样的Lisp程序,文中的错误和不足在所难免,希望读者批评指正,感谢!

参考

The Roots of Lisp
The Nature of Lisp
Why Lisp macros are cool, a Perl perspective
Wikipedia: Language-oriented programming
《实用Common Lisp编程》
《冒号课堂 – 编程范式与OOP思想》
您可能也喜欢:




编程语言汽车




基于JVM的语言正在开始流行




编程语言时间地理图




非常不错的编程技术教程




面向对象是个骗局?!


无觅

相关文章