关于编程语言的思考

jopen 12年前

  之前写了那么多 Haskell 的不好的地方,却没有提到它好的地方。其实我必须承认,我从 Haskell 身上学到了非常重要的东西,那就是对于“类型”的思考。虽然 Haskell 的类型系统有过于强烈的约束性,从一种“哲学”的角度看感觉“不自然”,但如果一个程序员从来没学过 Haskell,那么他的脑子里就会缺少一种重要的东西。这种东西很难从除 Haskell,ML,Coq,Agda 以外的其它语言身上学到。

  Haskell 教会我的东西

  一个没有学过 Haskell 的 Scheme 程序员,最容易犯的一个错误就是,把除#f(Scheme 的逻辑“假”) 以外的任何值都作为 #t(Scheme 的逻辑“真”)。很多人认为这是 Scheme 的一个“特性”,可是殊不知这其实是 Scheme 的极少数缺点之一。如果你了解 Lisp 的历史,就会发现在最早的时候,Lisp 把 nil(空链表)这个值作为“假”来使用,而把 nil 意外的其它值都当成“真”。这带来了逻辑思维的混乱。

  Scheme 对 Lisp 的这种混乱做法采取了一定的改进,所以在 Scheme 里面,空链表 '() 和逻辑“假”值 #f 被划分开来。这是很显然的事情,一个是链表,一个是 bool,怎么能混为一谈。Lisp 的这个错误影响到了很多其它的语言,比如 C 语言。C 语言把 0 作为“假”,而把不是 0 的值全都作为“真”。所以你就看到有些自作聪明的 C 程序员写出这样的代码:

int i = 0;  ...  ...if (i++) { ...}

  Scheme 停止把 nil 作为“假”,却仍然把不是 #f 的值全都作为“真”。Scheme 的崇拜者一般都告诉你,这样做的好处是,你可以使用

(or x y z ...)

  这样的表达式,如果其中有一个不是 #f,那么这个表达式会直接返回它实际的值,而不只是 #t

  然而他们没有看到的是,其实这个表达式所要达到的“目的”,其实有更加简单而直接的方法,而不需要把非 #f 的值都作为“真”。你只需要定义一个函数:

(define orf    (lambda (ls)      (cond        [(null? ls) #f]        [else        (let ([v (car ls)])           (if (not (eq? v #f))               v               (orf (cdr ls))))])))

  之后你就可以这样调用它:(orf '(#f #f 0 #f "foo"))。这会在遇到 0 的时候返回它,因为 0 是这个链表里第一个不是 #f 的值。如果链表里全都是 #f 它就返回 #f

  这比起 Scheme 的 or 来,不但效率一样,而且还有一个好处。那就是这个 orf是一个函数,而 or 是一个宏。所以你没法把 or 作为参数传递给另一个函数。你没法使用像 (map or ...) 这样的写法。而这个 orf 由于是一个函数,所以可以被作为值,任意的传递给另一个函数。

  所以虽然我看清楚了 Haskell 的缺点,我不想再使用它,我对它的程序员的高傲态度也感到厌倦,然而我的脑子里却留下了它教会我的东西。对 Haskell 的理解,让我成为了一个更好的 Scheme 程序员,更好的 Java 程序员,更好的 C++ 程序员,甚至更好的 shell 脚本程序员。我能够在任何语言里再现 Haskell 的编程方式的精髓。

  所以怎么说呢,我觉得每个程序员的生命中都至少应该有几个月在静心学习 Haskell。学会 Haskell 就像吃几天素食一样。每天吃素食显然会缺乏某些营养,但是每天都吃荤的话,你就永远意识不到身体里的毒素有多严重。

  专攻一门语言的害处

  我曾经对人说 C++ 里面其实有一些好东西,但是我没有说的是,C++ 里面的坏东西实在太多了。

  有些人从小写 C++,一辈子都在写 C++。这样的结果是,他们对 C++ 里面的“珍珠”掌握的非常牢靠,以至于出现了一种“脑残”的现象——他们没法再写出逻辑清晰的程序。(这里“珍珠”是一个特殊的术语,它并不含有赞美的意思。请参考这篇博文。)

  比如,很多 C++ 程序员很精通 functor 的写法,可是其实 functor 只是由于 C++ 没有 first-class function 而造成的“变通”。C++ 的 functor 永远也不可能像 Scheme 的 lambda 函数一样好用。因为每次需要一个 functor 你都得定义一个新的 class,然后制造这个 class 的对象。如果函数里面有自由变量,那么这些自由变量必须通过构造函数放进 functor 的 field 里面,这样当 functor 内部的“主方法”被调用的时候,它才知道自由变量的值。所以为此,你又得定义一些 field。麻烦了这么久,你得到的其实不过是 Scheme 程序员用起来就像呼吸空气一样的 lambda。

  这些“精通” functor 的 C++ 程序员,认为会用 functor 就说明自己水平高。殊不知 functor 这东西不但是一个“变通”,而且是从函数式语言里面“学”过来的。在最早的时候,C++ 程序员其实是不知道 functor 这东西的。如果你考一下古就会发现,C++ 诞生于 1983 年,而 Scheme 诞生于 1975 年,Lisp 诞生于 1958 年。Scheme 比 C++ 的出现整整早了 8 年,然而 Scheme 一开始就有 lexical scoping 的 lambda。functor 只不过是对 lambda 的一种绕着弯的模仿。实际上 C++ 后来加进去的一些东西(包括 boost 库),基本上都是东施效颦。

  记得 2011 年 11 月 11 日的良辰吉日,C++ 的创造者 Bjarne Stroustrup 在 Indiana 大学做了一个演讲,主题是关于 C++11 的新特性。当时我也在场,主持人 Andrew 是 boost 库的首席设计师之一(他后来有段时间当过我的导师)。他连夸 Stroustrup 会选日子,只是遗憾演讲时间没有定在 11 点。

  虽然我对 Stroustrup 的幽默感和谦虚的态度感到敬佩,但我也看出来 C++11 相对于像 Scheme 这样的语言,其实没有什么真正的“新东西”。大部分时候它是在改掉自己的一些坏毛病,然后向其它的语言学习一些东西。其实到最后,它仍然不可能达到其他语言那么原汁原味的效果。然而,由于 C++ 的普及程度之高,现成的代码之多,它的地位和重要性还是一时难以动摇的。所以“先辈的罪”,我们恐怕要用很多代人的工作才能弥补。

  那么 C++ 有什么其他语言没有的好东西呢?我还是下次再讲吧。

  多学几种语言

  我今天想说其实就是,没有任何一种语言值得你用毕生的精力去“精通”它。每个人都应该学习多种语言,这样才不至于让自己的思想受到单一语言的约束,而没法接受新的,更加先进的思想。这就像每个人都应该学会至少一门外语一样,否则你就深陷于自己民族的思维方式。有时候这种民族传统的思想会让你深陷无须有的痛苦,却无法自拔。