老赵书托(2):计算机程序的构造与解释
2009-07-15 00:11 Jeffrey Zhao 阅读(47875) 评论(102) 编辑 收藏 举报我要推荐的第一本书便是大名鼎鼎的《Structure and Interpretation of Computer Programs》,在国内可以买到中译版,即机械工业出版社的《计算机程序的构造与解释》。
抽象
豪不夸张地说,这是一本影响了好几代程序员的书。自从上世纪80年代MIT开始使用这本书作为教材开始,它使用Lisp语言——直到前两年才被Python取代,但是使用哪本教材不得而知,由这个侧面也可见SICP这本书的影响力有多么深远。在技术日新月异的计算机行业,有多少教材可以经得起20年的考验?
至于为什么要推荐这本书,还是要从这本书在讲什么东西谈起。您觉得,对于一个程序员来说,他最需要培养哪些能力?需要了解哪些知识?如果要我回答,我会说,一个合格的程序员需要一定要对计算机算法与数据结构有较为踏实的了解(这点在以前的文章中也重复了很多次)。至于“操作系统”、“计算机网络”、“编译原理”等课程是否重要?我不知道。这些课程都被广泛接受,所以它们肯定是有用的,但是如果您追问我它们的具体作用,我无法清晰明确地告诉您答案(例如,“编译原理”对普通程序员有什么作用?)——所以我不知道。当然,以后我还是会推荐一些这方面的书籍(因为“需要与否”其实都是个“尺度”和“方向”问题),到那时我们再继续谈论这方面的话题。
不过我可以肯定的是,一个合格的程序员(无论前台/后台,系统/应用),必须要有一定的分析问题解决问题的能力——或者说,抽象的能力。抽象是使用程序解决问题的必备手段之一。例如:
- 您是否可以把一个多级的系统分类,理解为一颗树,然后用树或图的方式来处理它?
- 如果让您解八皇后问题,或者走一个简单的迷宫,基本上不太会难倒你,但是您可以把自己的思路使用程序表现出来吗?
- 领域驱动设计的一个重要部分,便是将真实世界中的“领域”提炼成模型,再使用计算机语言实现出来。
需要抽象能力的情况,数不胜数。而SICP这本书,其目标便是培养您的抽象能力,自然还有使用基本的手段进行组合来解决问题的能力。这点正如书中1.1节The Element of Programming中所述:
Thus, when we describe a language, we should pay particular attention to the means that the language provides for combining simple ideas to form more complex ideas. Every powerful language has three mechanisms for accomplishing this:
- primitive expressions, which represent the simplest entities the language is concerned with,
- means of combination, by which compound elements are built from simpler ones, and
- means of abstraction, by which compound elements can be named and manipulated as units.
函数式编程
全书使用Lisp进行教学,这是一门函数式编程语言。有人说,函数式编程语言适合在实验室里把玩,不适合开发大型工程——我觉得这还是一个怎么看的问题。这里谈一个我的亲身经历:我在大学里也有课程是讲述LISP语言,但当时的感觉只是“一种比较新奇的语言”,至于它有什么用,它有什么帮助我根本一概不知。然而,经过了“工程”的磨练和实践之后,我反而慢慢体会到函数式编程的优势来。在我看来,函数式编程对于实际工程上的影响,一个主要的方面在于它可以使用更小粒度的抽象单元。对于面向对象编程来说,其抽象的最小单元为“类”和“实例”。试想如果您的程序想要展开“交互”,无论如何都必须从一个“实例”和“类”上面发起。而对于函数式编程来说,它最小粒度的抽象为“函数”。例如,您可以把一个方法作为另一个方法的参数或返回值(所谓高阶函数),而一段逻辑的实现完全可以通过“小方法”的组合来进行。不要小看这种抽象级别的改变,它会大大影响系统API的设计。
这里举一个示例。一个小问题作为示例:“求出a到b之间所有整数之和”。这很容易,您可以会这么做。
static int Sum(int a, int b) { int sum = 0; for (int i = a; i <= b; i++) sum += i; return sum; }
那么,“求出a到b之间所有整数的平方之和”或“绝对值之和”呢?当然,您可以再写两个方法。但是,从函数式编程角度来说,这完全是一个可以复用的逻辑:
static int Sum(Func<int, int> f, int a, int b) { int sum = 0; for (int i = a; i <= b; i++) sum += f(i); return sum; }
您可以将一个函数(在.NET里用委托表示)作为参数传入Sum方法,在调用时只需传入f的实现即可:
int i = Sum(x => x * x, 1, 3); // 14 int j = Sum(x => Math.Abs(x), -3, 3); // 12
甚至于,我们可以将其“部分应用(partial application)”1。简单说来,“部分应用”是将函数的部分参数固定,以得到一个新的函数:
static Func<int, int, int> SumCurry(Func<int, int> f) { return (a, b) => Sum(f, a, b); }
这样我们就可以使用SumCurry来获得新的函数了2:
var sumOfSquare = SumCurry(x => x * x); // int i = sumOfSquare(1, 3); var sumOfCube = SumCurry(x => x * x * x); // int j = sumOfCube(1, 3);
如果您理解了上面的代码,其实您已经对函数式编程更细致的抽象能力有所体会了。如果是面向对象编程,您需要怎么做呢?首先,您可能需要定义一个抽象类SumCalculator,其中有一个抽象方法为F。我们要复用算法,就必须构造SumCalculator的子类,提供F的具体实现。哪种做法简单,哪种做法繁琐,一目了然。
目前函数式编程几乎已经成了高级语言的必备特性了,如C#,F#,甚至颇有代替Java语言之势的Scala中也包含了相当的函数式编程能力。事实上,我认为出现这种趋势的一个重要原因,便在于人们之前对面向对象语言的抽象能力寄予过高期望,而这种期望的破灭(或者说“冷静”)使得许多人的注意力又回到了更容易“组合”和“复用”的函数式编程理念上。而且,其实人们从来没有放弃过对小粒度的事物的热爱。例如很多人喜欢C语言的原因,便是因为它没有庞大的架构,可以通过各种方法的组装来编写程序。而Unix编程艺术之一,便是大量小程序的组合复用。
您对函数式编程的重要性还有所怀疑吗?如果您觉得上面的例子还有些“玩具”感觉的话,您还可以参考我之前实现的CacheHelper,AsyncTaskDispatcher(上,下)或者微软的并行库——还有Matthew Poswysocki发起的“反对for行动”。您不妨思考一下,如果没有函数式编程特性,又该如何实现这些功能呢?
自然,函数式编程的优点远不止这一个。例如函数式编程中“无副作用”的纯函数,对于目前愈发热烈的并行环境也有重大意义。这些就要靠您来自行挖掘了3。
补充建议
最后,还是补充一些我自己的建议吧4。
首先,SICP是一本教科书,里面的示例和习题都是经过精心设计的,几乎可以说都是有针对性地培养各种能力。换句话说,如果您太“功利”地阅读这本书,可能会让您觉得失望。您没法从中学到如何开发一个网站,开发一个记事本,如何绘图,它完全是在锻炼程序员的基本能力,而不是“技术”。有时候,您可以把它当作一本数学书看,里面的题目也经常和数学有关——别担心,似乎高中数学水平应该足够了吧。另外,我建议您在看这本书时,最好可以挑一个风和日丽天气,准备一台笔记本去附近大学的图书馆里找个座位,像一个学生那样参加自习,慢慢地看耐心的看——没有笔记本?那也没关系,一个拔网线的台式机也可以起到差不多的效果。我也欢迎大家和我一起探讨其中的题目——虽然有相当部分内容我也不会。:P
看一本书,不一定要从头到底全部看完。SICP全书共分五章,我建议可以认真阅读前三章——没时间的话就精读前两章“过程抽象”与“数据抽象”。如果您有时间的话,也可以把第四章看完。至于第五章,有人说是SICP的精华所在,但是我认为啃下第五章的投入产出比相对前几章来说就相对较低了(第四和第五章使用Lisp实现一个解释器及一个简单的CPU逻辑实现,很难,不过这的确是Lisp最“美”最能体现出完备性的地方)。如果,我是说如果,您在阅读前两章时较为困难,也不妨先看一下《Simply Scheme: Introducing Computer Science》,您可以把它看作是SICP的基础5。值得一提的是,SICP和SSICS都在互联网上公开——不得不敬佩他们对学术推广的态度。
您在学习SICP这本书时,也可以选择配合相关的公开视频。您有两个选择,一是Berkeley的上课视频,二是MIT的公开课件。前者的讲述较为轻松有趣,相对容易理解一些,我看了大半;而后者为SICP原书作者Hal Abelson 和Gerald Jay Sussman为Hewlett-Packard公司员工培训时的录像,我感觉更为体系、理论、也相对较为难懂——当然,这只是我看了Lecture 1的两段录像后的感觉。此外,北大也开设了《程序设计技术和方法》课程,使用SICP作为教材,相信也是不错的参考。
最后便是开发环境了。SICP使用Lisp语言,而在实验时,您不妨使用Scheme,它是Lisp语言的两种常见方言之一(还有一种是Common Lisp)。您可以使用GNU/MIT Scheme作为编译/解释器,不过我使用的是IronScheme,它基于DLR,也是.NET平台下的另一个编程语言实现。不过话说回来,其实我在做SICP的习题时使用最多的还是F#,它是由微软研究院发明的又一种.NET语言,同时拥有强大的函数式编程和面向对象能力。此外,我现在对于Haskell也有相当兴趣,这个老牌的纯函数式编程语言也慢慢地回到人们视线中来。我使用的参考书是《Real World Haskell》,它获得了Jolt大奖最佳技术书籍,希望能从中获取更多灵感。最关键的是,这本书也在互联网上完全公开——这是一种什么样的精神!
相关文章
- 老赵书托(1):写在前面
- 老赵书托(2):计算机程序的构造与解释
- 老赵书托(3):深入理解计算机系统
注1:原本我这里写了“柯里化”,有朋友指出,严格说来这其实应该是“部分应用”。关于这点有待稍后确认。
注2:其实,如果您要在C#中实现部分应用,更通用的做法可能是构造如下的扩展方法:
public static Func<T2, T3, TResult> Currying<T1, T2, T3, TResult>( this Func<T1, T2, T3, TResult> f, T1 t1) { return (t2, t3) => f(t1, t2, t3); }
注3:这段文字只是为了说明FP并非玩具,是对您有切实帮助的——当然您也无需将其理解为“不学FP非好汉”的观点。关于这方面话题,您可以浏览怪怪同学的文章《嗯嗯,关于SICP》,一起品味一下。
注4:这些建议,乃至整篇文章都是根据我个人体会总结得来,有相当的主观成分。欢迎提出不同意见,多种看法的汇总对读者是最有价值的。
注5:这本书我只是简单的浏览过部分章节,因此感觉可能会有所偏差——当然,“SICP基础书籍”这个观点也是SSICC一书给自己的定位。