转自<王垠>教主...
很多人都关心这个问题,来信问我。我一直想总结一下经验,让大家可以免得走弯路。今天终于开始写这篇文章。我的文章一般都会在发表之后不断改动,所以如果转载请只给出链接,以便得到最新的版本。
面向对象语言不适合入门
有的人抱怨很多学校开始教授 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 那么强大的功能。而且 Python 还有别的一些糟糕的设计。我实现过 Python 的静态分析,所以我知道这些毛病带来的麻烦。事实上,像 Python,JavaScript, Clojure 这样的语言正在破坏人们对各种优秀的函数式语言概念的印象。因为它们打着“lambda”,“closure”,“pure”,“lazy”这些美名,却没有达到它们真正的本质。
底层语言不适合入门
那么是否 C 就会好一些呢?其实也不是。很多人推崇 C,因为它可以让人接近“底层”,也就是接近机器的表示,这样就意味着它速度很快。这里其实有三个问题:
接近“底层”是否对于初学者是好事?
“速度快的语言”是什么意思?
接近底层的语言是否一定速度快?
对于第一个问题,我的答案是否定的。其实编程最重要的思想是高层的语义(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 有高效的编译器实现,使得它能生成很好的代码。顺便替这个太低调的教授吹个牛,Robert Constable 是计算机科学鼻祖之一的 Stephen Kleene 的学生,他的主要贡献是 NuPRL 定理证明系统。现在世界上很多著名的程序语言专家都是他的学术后裔,比如 Robert Harper,Benjamin Pierce,Greg Morrisett。
从上面的例子,你也许已经可以看出,其实接近底层的语言不一定“速度”就快。因为编译器这种东西其实可以有很高级的智能,甚至可以超越任何人能做到的底层优化。但是编译器还没有发展到可以代替人来制造算法的地步(虽然有些技术,比如 supercompilation,有可能自动生成某些新的算法)。所以现在人需要做的,其实只是设计和优化自己的高层算法。
学习两种函数式语言
那么我推荐什么样的语言呢?首先我觉得相对而言,函数式的语言特别适合入门者。因为它们不但让学生专注于算法和对问题的解决,而且没有面向对象语言那些思维的限制。那么现在的问题是,哪一种函数式语言。这是一个很难回答的问题,因为没有一种函数式语言拥有所有的优点,而它们的狂热分子们经常把缺点也说成是优点,结果你还是会被误导。所以我觉得,初学者需要学习至少两种函数式语言。不要被我吓倒了,你并不需学习这些语言的所有细枝末节,而只需要学习最精华的部分。我后面会提一下哪些是精华的,哪些是最开头没必要学的。
其实真正严格意义上的函数式语言不多。最可信而又广泛使用的的函数式语言是 Scheme, Haskell, OCaml,SML, Clean 等。就我的观点,首先可以从 Scheme 入门,然后学习一些 Haskell (但不是全部),之后其它的也就触类旁通了。
从 Scheme 入门而不是 Haskell,首先是因为 Scheme 没有像 Haskell 那样的静态类型系统 (static type system)。并不是说静态类型不好,但是我不得不说,Haskell 那样的静态类型系统,还远远没有发展到可以让人可以完全的写出符合事物本质的程序来。我亲自实现了 Haskell 所使用的 Hindley-Milner 类型系统以及比它更高级的系统,所以我知道这个系统能让人写出什么样的程序,我也知道它的弱点。一些重要的概念比如 Y combinator,没法用 Haskell 直接写出来。当然你可以在 Haskell 里面使用作用类似 Y combinator 的东西(比如 fix,或者利用它的 laziness),但是这些并不揭示递归的本质,你只是在依靠 Haskell 已经实现的递归来进行递归,而不是从无到有制造出递归来。所以这样你又落入了学习实现的死胡同。
另外,Haskell是一个“纯函数式” (purely functional) 的语言,所有的“副作用”(side-effect),比如打印字符到屏幕,都得用一种深奥而偏僻的概念叫 monad 实现。这种概念其实并不是本质的,它所有的功能都可以通过“状态传递” (state passing) 来实现。你甚至可以说 monad 是 Haskell 的一个“设计模式”。通过写状态传递程序,你可以清楚的看到 monad 的本质。但是过早的知道 Haskell 对此的实现细节,并不有助于理解函数式程序设计的本质。
那么为什么又要学 Haskell?那是因为 Haskell 含有 Scheme 缺少的一些东西,并且没有 Scheme 设计上的一些问题。比如:
缺少模式匹配:Scheme 没有一个标准的,自然的模式匹配 (pattern matching) 系统,而 Haskell 的模式匹配是一个非常优美的实现。
类型模糊:比如 Scheme 把所有不是 #f (false)的值都作为 true,这是不对的。Haskell 里面的 Boolean 就只有两个值:True 和 False。Scheme 程序员声称这样可以写出简洁的代码,因为 (or x y z) 可以返回一个具体的值,而不只是一个布尔变量。但是就为了在少数情况下可以写出短一点的代码,是否值得付出如此沉痛的代价?我看到这个设计带来了很多无需有的问题,包括很多 Scheme 的静态分析 (比如 control-flow analysis) 论文所考虑的问题。
宏系统:宏 (macro) 通常被认为是 Lisp 系列语言的一个重大优点。但是我要指出的是,它们并不是必要的,至少对于初学者是这样。其实如果一个语言的语义设计好了,你会几乎不需要宏。因为宏的本质是让程序员可以自己修改语言的设计,添加新的构造。但是宏的主要缺点是它把修改语言这种“权力”给人滥用了。其实只有非常少的人具有足够的智慧可以设计程序语言,如果让普通程序员使用宏,那么程序将变得非常难以理解。这个以后我可能专门说一下。
(注意,这些是我自己的观点,并不代表 IU 的 Scheme 设计者们的观点。)
Haskell 的这些设计,是来源于对逻辑和类型的严格思考。Haskell 的类型系统,虽然不是完美的,但是其实已经很不错了。至少它能让初学者体会到什么叫做类型系统,以及它的好处,从而在将来可以使用具备更加强大类型系统的语言(比如 Agda 和 Coq)。实际上,像 C++ 这样的语言已经开始向 Haskell 学习。比如 C++ 的 “concepts” 实际上就是 Haskell 的 type class。所以学会了 Haskell 也有利于理解 C++ 最新概念的本质。
那么,Haskell 的哪些内容可以在初学的时候暂时跳过呢?首先是 monad。你可以只在 Haskell 里面写 pure 的函数,或者通过传递状态来做一些事情,比如实现解释器或者逻辑语言之类的。其次是 GHC 的一些扩展。不是说这些没用,但它们不是最关键的东西。它们的存在往往是为了弥补一些 Hindley-Milner 类型系统存在的缺点。
推荐的书籍
《The Little Schemer》:我不想给他打广告,但是我确实觉得 Dan Friedman 的 The Little Schemer (TLS) 是最好的 Scheme 教材。它完全忽略了上面提到的 Scheme 的毛病,直接进入最关键的主题:递归和它的基本原则。在第九章,你会学到如何从无到有推导出 Y combinator。我做过一个幻灯片,演示的就是这里的推导过程。这本书不但很薄,很精辟,而且相对于其他编程书籍非常便宜(在美国才卖23美金)。
《SICP》:The Little Schemer 其实是比较难的读物,所以我建议把它作为下一步精通的读物。Structure and Interpretation of Computer Programs 比较适合作为第一本教材。但是我需要提醒的是,你最多只需要看完第三章。因为从第四章开始,作者开始实现一个 Scheme 解释器,但是作者的实现并不是最好的方式,你可以从别的地方更好的学到这些东西。具体在哪里学,我还没想好(也许我自己写个教学也说不定)。不过也许你可以看完 SICP 第一章之后就可以开始看 TLS。
《A Gentle Introduction to Haskell》:对于 Haskell,我最开头其实看的是 A Gentle Introduction to Haskell,因为它特别短小。当时我已经会了 Scheme,所以不需要再学习基本的函数式语言的东西。我从这个文档学到的只不过是 Haskell 对于类型和模式匹配的概念。
过度到面向对象语言
那么如果从函数式语言入门,如何过渡到面向对象语言呢?毕竟大部分的公司用的是面向对象语言。如果你真的学会了函数式语言,你真的会发现面向对象语言已经不在话下。函数式语言的设计比面向对象语言简单和强大很多,而且几乎所有的函数式语言教材(比如 SICP)都会教你如何实现一个面向对象系统。你会深刻的看到面向对象的本质以及它存在的问题,所以你会很容易的搞清楚怎么写面向对象的程序,并且发现一些窍门可以避开它们的局限。你会发现,即使在实际的工作中必须使用面向对象语言,如果你主动的避免面向对象的思维方式,你甚至可以用面向对象语言做出更好的设计,因为面向对象的思维带来的大部分是混乱和冗余。
深入底层
那么是不是完全不需要学习底层呢?当然不是。但是一开头就学习底层硬件,就会被纷繁复杂的硬件设计蒙蔽头脑,看不清楚本质上简单的原理。
在学会高层的语言之后,可以进行语义学和编译原理的学习。简言之,语义学 (semantics) 就是研究程序的符号表示如何对机器产生“意义”,通常语义学的学习包含 lambda calculus 和各种解释器的实现。编译原理 (compilation) 就是研究如何把高级语言翻译成低级的机器指令。编译原理其实包含了计算机的组成原理,比如二进制的构造和算术,处理器的结构,内存寻址等等。但是结合了语义学和编译原理来学习这些东西,会非常高效。因为你会直观的看到为什么现在的计算机系统会设计成这个样子:为什么需要寄存器(register),为什么需要堆栈(stack),为什么需要堆(heap),它们的本质是什么。这些甚至是很多硬件设计者都不明白的问题,所以它们的硬件里经常含有一些没必要的东西。因为他们不理解语义,所以经常不明白他们的硬件到底需要哪些部件和指令。但是从高层语义来解释它们,就会揭示出它们的本质,从而可以让你明白如何设计出更加优雅和高效的硬件。
这就是为什么一些程序语言专家后来也开始设计硬件。比如 Haskell 的创始人之一的 Lennart Augustsson,后来设计了 BlueSpec,一种高级的硬件描述语言,可以 100% 的合成 (synthesis) 为硬件电路。Scheme 也被广泛的使用在硬件设计中,比如 Motorola 和 Cisco,它们都是 Chez Scheme 的忠实用户。
这基本上就是我对程序语言入门的建议。我可能还会修改其中一些内容。有问题的话欢迎给我留言或者发邮件到我的信箱:shredderyin@gmail.com。谢谢大家。
转自:http://blog.sina.com.cn/s/blog_5d90e82f01015271.html