读书导读 · CC BY-SA 4.0

《计算机程序的构造和解释》全书摘要与逐章导读

《计算机程序的构造和解释》是一本讲“怎样让大型程序始终可以被人理解”的教科书。作者在序言里说,它的主题不是某门语言的语法,也不是巧妙的算法,而是控制软件智力复杂度的方法:建立抽象、约定通用接口,在问题需要时发明一门小语言。本页依据第二版(1996),MIT 出版社以 CC BY-SA 4.0 在网上免费公开全文。

CC BY-SA 4.0

引文按 CC BY-SA 4.0 使用;本页文字也按同一协议授权,引文译文属于改编作品。免费阅读原书。

《计算机程序的构造和解释》讲了什么AI 辅助

形式化证明无法扩展:只有组织技术才能造出大型系统

我们如何控制程序里的复杂性?本书的回答是:靠抽象,最终靠构造语言。它从这样的主张出发:编程语言是向人类读者表达方法论的媒介,而使大型程序成为可能的是组织技术,不是形式化证明。三种机制——基本表达式、组合的手段、抽象的手段——描述任何强大的语言,而它的求值规则是递归的。抽象随后被用到过程上,用到复合数据上(理由相同),也用到由构造函数和选择函数的条件所定义的表示上,所以仅凭过程就能在抽象屏障和约定的接口之后实现数据。通过镜像来给世界建模,会引来赋值和局部状态,它们换来模块化,代价是引用透明性和代换模型。本书于是上升一层:决定含义的求值器本身是个普通程序,所以程序员成了语言设计者,而别的求值规则,只是对它的修改。因为元循环求值器借用了宿主的控制结构,本书又下降到寄存器机器,在那里控制、栈和递归都变得显式。

《计算机程序的构造和解释》的核心论点

全书捍卫的 10 个论点,按论证需要的顺序排列。

  1. 因为形式化的正确性论证无法随程序规模扩展,使大型程序成为可能的,实际上是对组织技术和抽象技术的掌握,而不是证明。

    “比什么都重要的是,发现并掌握强大的组织技术,能加快我们创造大型而有意义的程序的能力。”

    “More than anything else, the uncovering and mastery of powerful organizational techniques accelerates our ability to create large, significant programs.”

    Foreword查看原文 →
  2. 编程语言是向人类读者表达方法论的媒介,所以评判程序应当像评判写作,而不是评判给机器的指令。
  3. 每一种强大的语言,只靠三种机制就能恰当地描述——基本表达式、组合的手段、抽象的手段——因而它的求值规则是递归的。
  4. 数据结构的统一性胜过可声明结构的泛滥,因为统一的数据使过程不特异,可以自由组合。
  5. 复合数据的理由,与复合过程恰好相同;一种表示由其构造函数和选择函数所满足的条件来定义,而不是由任何存储布局来定义,所以仅凭过程就能实现数据。
  6. 把对表示的依赖限制在少数几个接口过程里,使程序可维护,也让设计可以在表示的选择尚未确定之前就往前推进。
  7. 程序的结构应当镜像被建模世界的结构,这样扩展模型就只意味着加上新的符号类比物。
  8. 赋值通过给对象局部状态来换取模块化,但代价是引用透明性、代换模型,以及对同一性、时间和并发的可处理的推理。
  9. 随着问题增长,没有哪种固定的语言是足够的,而且既然决定表达式含义的求值器本身只是另一个程序——这使几乎任何像样的程序,都是某种语言的求值器——程序员就必须成为语言设计者,靠修改求值器来获得新的求值规则。
  10. 因为元循环求值器悄悄借用了宿主的控制结构,要理解计算就得下降到专门造的寄存器机器,那里迭代只需要寄存器,递归却要一个栈,而复杂的操作仍然可以合理地当作可展开的基本操作。

逐章导读

每一章都在本页:论证怎样展开、关键论点、常见误读和核心概念。从目录里选一章。

第 1 章:构造过程抽象

代换模型怎样解释复合过程的应用?

第 1 章是怎样展开的AI 辅助

开场:一个看不见的问题。 这一章开头讲的不是语法,而是一个困境。程序指挥一个没有人看得见的计算过程,这意味着程序员的核心技能,是预见一个小错误在下游会造成什么——而作者的例子坚持说,惩罚与错误的大小并不成比例:设计程序里的一个 bug,可能以坍塌的飞机或水坝告终,或是一个机器人毁了自己。接着,Lisp(Scheme 方言)被论证为合适的载体,并从这个框架里提炼出本章的组织性主张:任何真正有力的语言都恰好提供三样东西——基本表达式、组合的手段、抽象的手段。开场的其余部分,只是把这个三元组一步步落到实处。表达式与递归的求值规则先出场,被画成一棵树,值沿着树向上渗透,所以对 (* (+ 2 (* 4 6)) (+ 3 5 7)) 求值,就是把一条规则用到四个组合式上。然后是 define,最简单的抽象,让 circumference 代表 62.8318——不过作者小心地称命名只是一种“有限的”抽象,而正是这个压力,让复合过程成为下一步。

第一个展开:一个模型,及其坦率的免责声明。 有了复合过程,本章需要说明应用一个复合过程“意味着”什么,于是给出代换模型。这里要紧的是紧随其后的限定。代换是给读者推理用的脚手架,不是对解释器的报告:真实的实现是在一个局部环境里约束形式参数(推迟到第3章和第4章),而一旦过程操作可变数据,这个模型就明说会垮掉。作者把这种“超越”说成普通的科学实践,而不是缺陷——这一点值得记住,因为自然的误读是把文本替换当作机器的图景。从这个模型出发,本章分叉到应用序对正则序,然后是 cond/if 和逻辑谓词,这给了它足够的机制去尝试做点真东西。

转向方法:牛顿法求平方根。 那件真东西是平方根,它承载了本章最尖锐的概念区分。平方根的数学定义,即使音译成伪 Lisp,也是完全合法的,却几乎没有告诉你怎样去找一个。所以计算过程与数学函数不是同一类对象,因为它必须是“有效的”——说明式知识对命令式知识——而牛顿法补上了缺失的“怎么做”。造好之后,本章马上把这个结果转成一课关于打包的功课:过程是黑盒,有约束变量和自由变量,而块结构让 good-enough? 和 improve 住在 sqrt 里面,既够不到用户,也够不到别的、正在写他们自己的近似例程的程序员。词法作用域随后完成这一步:因为 x 由 sqrt 自己约束,嵌套的辅助过程就坐在它的作用域里,把它当作自由变量引用,所以 x 不必作为额外的参数传来传去。

第二个展开:作为测量对象的计算过程。 本章现在把口气从“过程意味着什么”换成“过程代价几何”。斐波那契和换零钱给出演示:树形递归容易写,却指数级地浪费,因为朴素的 fib 在许多分支上重复计算同样的小情形,而最终落到 (fib 1) 或 (fib 0) 的叶子数,恰好是 Fib(n+1)——本身就是指数的。这种不对称是最难忘的部分:内存只需要保存沿着树往下的当前路径,所以空间只与 n 成正比地增长,而时间爆炸。迭代随后显得廉价,增长阶的记号(theta)提供了词汇,而逐次平方和欧几里得算法给出真正对数级的替代。这个序列以费马检验收尾,它把测量推进一个新的范畴:随机选一个 a < n,检查 aⁿ 模 n 是否等于 a,然后重复。只有失败是决定性的——卡迈克尔数对每一个 a 都满足这个同余式,所以通过什么也证明不了——但它们稀少得足以让检验保持实用,而有了不可欺骗的变体,每次独立抽取都会使被误导的概率相乘,所以一次通过胜过一半一半,两次胜过四分之三,足够多次试验可以把出错概率降到想要的低。这就界定了概率性算法,一类保证是统计性而非演绎性的方法。

最后的展开:抽象用到自己身上。 高阶过程是通过一个从反面出发的论证到来的。一门没有办法给“立方”命名的语言,仍然可以计算立方,却会迫使每个人停留在一次次乘法的层次上;反过来,一门不能把过程当作参数的语言,只能陈述一个模式的实例,永远陈述不了模式本身。sum 就是演示——给定一个项函数、一个取下一个下标的函数,以及上下界 a 和 b,它把 sum-cubes、sum-integers 和 pi-sum 收成三次调用,就像求和记号 sigma 表达一般的求和。本章随后把这个手段叠加起来:sum 变成 integral;lambda 和 let 处理掉有名字的辅助过程;half-interval 和 fixed-point 表达的是一般方法,而不是特定的计算;而 average-damp 和 deriv 靠“返回”过程越过了剩下的那条线——把 average-damp 用到 square 上,得到一个对 10 返回 55 的过程,而把 fixed-point 同平均阻尼复用,立刻就产生了 cube-root。

收束:为挣得的东西命名。 最后一个运动是把线索系在一起,而不是增加材料。牛顿法由 deriv 重建为一个不动点过程,然后两个平方根程序——本章前三分之一手工做出来的那个,和牛顿版本——都从同一个 fixed-point-of-transform 里落了出来,这是本章自己的证明:它一直是在抽象方法,而不只是计算。在此基础上,本章命名了一等公民地位及其四项权利:过程可以被命名、被当作参数传递、被当作结果返回、被存进数据结构(最后一项要到后面才演示)。Lisp 四项都给了;作者承认这使高效实现变复杂,并论证表达力偿还了这笔代价。这一章于是恰好结束在它开头的那个三元组所指向的地方:抽象的手段被转用在它们自己身上,使抽象不再只是被构造,而是被操作。

关键论点,以及支撑它们的原文

论点 1

因为程序指挥的是一个没有人看得见的过程,程序员的核心技能,就是预见一个小小的错误会造成什么。

作者指出计算机之外的后果:计算机辅助设计程序里一个小小的 bug,可能以一架飞机或一座水坝的坍塌,或一个机器人毁了自己告终。这些案例是说明性的,不是测量得来的,但它们说明了一点:小错误的惩罚,与它的大小并不成比例。

“即使程序里很小的错误(通常称为 bug 或故障)也可能带来复杂而意想不到的后果。”

“Even small errors (usually called bugs or glitches) in programs can have complex and unanticipated consequences.”

Chapter 1 Building Abstractions with Procedures查看原文 →
论点 2

任何真正有力的语言都提供同样的三样东西:基本表达式,组合它们的办法,以及对结果进行抽象的办法。

作者逐个演示:define,最简单的抽象手段,让一整个复合操作可以用一个名字来调用,如 circumference 代表 62.8318;而对一个嵌套表达式如 ( (+ 2 ( 4 6)) (+ 3 5 7)) 求值,意味着把一条求值规则用到四个组合式上,可以画成一棵值向上渗透的树。与定义过程相比,用 define 命名仍然只是有限的抽象,这就是为什么接下来是过程定义。

“每一种强大的语言,都有三种机制来完成这件事:”

“Every powerful language has three mechanisms for accomplishing this:”

§1.1 The Elements of Programming查看原文 →
论点 3

代换模型是给读者思考用的脚手架,不是对解释器所做之事的报告——而且后来被取代是正常的,不是缺陷。

用代换解释过程应用之后,作者立刻加了限定:实际上形式参数是由局部环境来处理的,在第3章和第4章讨论,而一旦过程作用在可变数据上,这个模型就明说会垮掉。作者把这说成科学和工程中的标准做法:从有意简化的模型出发,期待自己会超越它们。

“代换的目的,是帮助我们思考过程的应用,而不是描述解释器真正怎样工作。”

“The purpose of the substitution is to help us think about procedure application, not to provide a description of how the interpreter really works.”

§1.1 The Elements of Programming查看原文 →
另外 3 个论点
论点 4

计算过程与数学函数不是同一类东西,因为它必须真的是有效的。

作者拿教科书里平方根的定义同一个程序作对比:即使改写成伪 Lisp,数学定义完全合法,却几乎没有告诉你怎样去找一个根。这就是命令式对说明式的区分,它催生了牛顿法,后者补上了缺失的“怎么做”。

“但数学函数与计算机过程之间有一个重要的区别。过程必须是有效的。”

“But there is an important difference between mathematical functions and computer procedures. Procedures must be effective.”

§1.1 The Elements of Programming查看原文 →
论点 5

让过程能接受过程作参数,使一门语言能陈述模式本身,而不只是它的实例。

作者先从反面论证:一门没有办法给“立方”命名的语言,仍然可以计算立方,却会迫使每个人停留在一次次乘法的层次上。反过来,一旦 sum 接受项函数和下一个下标的函数,连同上下界 a 和 b,sum-cubes、sum-integers 和 pi-sum 就都收成对这一个过程的调用,就像求和记号 sigma 表达一般的求和。

“我们的程序能够计算立方,但我们的语言将缺乏表达“立方”这个概念的能力。”

“Our programs would be able to compute cubes, but our language would lack the ability to express the concept of cubing.”

§1.3 Formulating Abstractions with Higher-Order Procedures查看原文 →
论点 6

授予过程完整的一等公民地位,使抽象本身可以被操作,而实现上的代价是值得的。

作者一步步建到这里:复合过程已经使方法显式化,而高阶过程让这些方法可以被进一步处理——把 average-damp 用到 square 上,得到一个对 10 返回 55 的过程,而把 fixed-point 同平均阻尼复用,立刻产生 cube-root;两个平方根程序随后都从同一个 fixed-point-of-transform 里落出来。结尾那四项权利的清单(可被命名、可当作参数传递、可当作结果返回、可存进数据结构,最后一项到后面才演示),界定了 Lisp 授予的东西,据说它使高效实现复杂化,同时在表达力上得到回报。

“与 average-damp 一样,deriv 是一个接受过程为参数、返回过程为值的过程。”

“Like average-damp, deriv is a procedure that takes a procedure as argument and returns a procedure as value.”

§1.3 Formulating Abstractions with Higher-Order Procedures查看原文 →

它是怎样运作的

本章论证所依靠的 3 个机制。

机制 1

为什么内层辅助过程可以丢掉参数 x

把 good-enough? 和 improve 定义在 sqrt 内部,它们的名字就使用者够不到,也别的写类似近似例程的程序员够不到。因为 x 由 sqrt 自己约束,嵌套的定义就坐在 x 的作用域里,能把它当作自由变量引用,所以 x 不必作为额外的参数传来传去。

机制 2

为什么树形递归在时间上爆炸、在空间上却不

朴素的斐波那契过程沿着许多分支重复计算同样的小情形:最终落到 (fib 1) 或 (fib 0) 的叶子数,恰好是 Fib(n+1),它本身指数级增长,所以步数也是。相比之下,内存只需保存沿着树往下的当前路径,所以空间只与 n 成正比地增长。

机制 3

反复的随机试验怎样买来近乎确定

一个合数通过的单次试验,留下真实的疑问;但对于大多数 a 的选择都没有合数能欺骗的检验变体,每次独立的随机抽取都使被误导的概率相乘。一次已经给出好于一半一半的把握,两次好于四分之三,所以跑足够多次试验,就能把出错概率压到想要的低,而永远不会成为证明。

常见误读

你可能以为

代换就是解释器真正运行一次过程调用的方式,所以想象实参被文本地粘贴进过程体,是机器的准确图景。

其实

代换是为了帮助读者推理过程应用;真实的实现是在局部环境里约束形式参数,而作者警告说,一旦过程作用在可变数据上,这个模型就不灵了——它是一个注定要被取代的第一模型。

“代换的目的,是帮助我们思考过程的应用,而不是描述解释器真正怎样工作。”

“The purpose of the substitution is to help us think about procedure application, not to provide a description of how the interpreter really works.”

§1.1 The Elements of Programming查看原文 →
你可能以为

一个数如果通过了费马检验,就证明了它是素数。

其实

只有失败是决定性的。存在这样的合数,对每个小于 n 的 a 都满足这个同余式,所以通过并不能绝对地确定什么;这样的数极其稀少,这使检验保持实用,而没有合数能欺骗的变体,才使反复试验可以把出错概率压到想要的低。

“更准确地说,如果 n 在费马检验中曾经失败,我们就能确定 n 不是素数。”

“More precisely, if n ever fails the Fermat test, we can be certain that n is not prime.”

§1.2 Procedures and the Processes They Generate查看原文 →

第 1 章的核心概念

计算过程
计算机内部一个看不见、无形、却真实存在的实体,在程序的指引下,随着演化操作数据。
适用处: 它不是物质,也看不见,却做着实实在在的智力工作,并能影响物理世界。
抽象的手段
让复合元素可以被命名、并当作单一单元来处理的语言特性。
适用处: 仅仅 define 只给出有限的抽象手段;过程定义才是更有力的那一种。
组合式
一个带括号的表,表示过程应用,运算符在最左,其余是运算对象。
适用处: 像 (define x 3) 这样的定义不是组合式;它们是特殊形式。
环境
解释器里“名字-对象”对的存储,使符号有了含义。
适用处: 这里叫作全局环境,因为以后的计算会涉及好几个环境。
特殊形式
自带求值规则的一种表达式类型,在一般的组合式规则之外。
适用处: define 是迄今引入的唯一特殊形式;这类规则的集合加上一般规则,就构成这门语言的语法。
复合过程
一个有名字的复合操作,它的形式参数代表应用时提供的实参。
适用处: 使用起来,与解释器内置的基本过程没有区别。
另外 22 个概念
代换模型
应用一个复合过程,就是把它过程体里的每个形式参数换成对应的实参,然后求值。
适用处: 仅仅是第1章里的过程的思维辅助;到第3章处理可变数据时就失效。
应用序求值
先对运算符和运算对象求值,再应用——这是 Lisp 实际使用的顺序。
适用处: 对非法的值,可能与正则序不同,如习题1.5。
正则序求值
先把运算对象表达式完全展开到过程体里,到最后才归约。
适用处: 可能重复求值;只有对可用代换模型描述的、值合法的应用,才与应用序得到同样的值。
条件分析
通过检验谓词,在不同的动作之间选择,在 Lisp 里用 cond 或 if 写出。
适用处: if 限于恰好两种情形;如果没有谓词为真,cond 的值是未定义的。
说明式知识与命令式知识
描述一样东西“是什么”,对描述“怎么计算”它。
适用处: 数学偏说明式,计算机科学偏命令式;一个合法的定义仍可能不是一个过程。
过程抽象
把过程当作黑盒:你依赖它计算出什么,而不是怎么计算。
适用处: 只对返回的值成立;含义仍取决于自由变量的名字。
约束变量与自由变量
形式参数是约束的,在过程体内可以改名;其他名字是自由的。
适用处: 改名必须一致,且必须避免捕获一个自由的名字。
块结构
把辅助定义放在使用它们的过程里面,让它们的名字保持私有。
适用处: 只解决最简单的名字打包问题;内部定义必须放在过程体的最前面。
词法作用域
内层过程里的自由变量,从外围定义的约束中得到它的值。
适用处: 取决于过程是在哪里定义的,而不是在哪里调用的。
树形递归
一个过程每次调用时多次调用自己,所以计算过程像树一样分叉。
适用处: 对数而言是浪费的,但对层次结构的数据很自然。
增长的阶
对所需资源怎样随问题规模增长的粗略度量。
适用处: 只是粗略的描述;忽略常数因子。
逐次平方
通过对中间结果求平方,把指数折半,把线性的工作变成对数的工作。
适用处: 需要分奇偶两种情形来处理一般的指数。
费马检验
随机选一个小于 n 的 a,检查 a 的 n 次方模 n 的余数是否为 a。
适用处: 失败证明是合数;通过只是强有力的证据,而卡迈克尔数会漏过去。
概率性算法
答案只是大概率正确的算法,靠反复的随机试验把错误压低。
适用处: 需要证明大多数随机选择都能暴露出非素数;否则重复并不能给错误设界。
高阶过程
以别的过程为参数、或把过程作为值返回的过程。 接受别的过程为参数、或返回过程为值的过程。
适用处: 只有在同一个模式在不同的项之间反复出现时才需要;单靠数值参数抓不住它。 当一个模式在许多函数中反复出现时有用;一次性的计算不需要这种抽象。
求和抽象
把几个求和过程的共同部分变成参数,就像数学里的求和记号 sigma 那样。
适用处: 适用于各过程只在项函数和下一个值函数上不同的情形。
lambda
一种特殊形式,创建一个过程而不在环境里给它起名字。
适用处: 除了命名之外,等价于 define;let 只是 lambda 应用的语法糖。
不动点
一个数 x,使 f(x) 等于 x,通过反复应用 f 直到值不再有多大变化而找到。
适用处: 反复应用不一定收敛;y 变为 x/y 只会来回振荡。
平均阻尼
把一个猜测同 f(猜测) 取平均,来减缓变化,帮助搜索收敛。
适用处: 有助于收敛,但不保证对每个函数都成立。
控制复杂性
选择能暴露可复用部分的表述,让思想本身变得清楚。 选择恰当的抽象层次,而不是尽可能抽象的层次。
适用处: 许多表述计算的是同一个过程;不同的是清晰度,而不是正确性。 抽象是一种判断,不是一个要最大化的目标。
一等公民地位
受到限制最少的元素:可以被命名、传递、返回,并存进数据里。
适用处: 这是语言设计的属性,不是哪一个过程的属性;Lisp 把它授予过程,付出了实现上的代价。
fixed-point-of-transform
一个过程,接受一个函数、对它的一个变换和一个猜测,返回变换后函数的不动点。
适用处: 只能抓住形如“先变换,再找不动点”的方法。

第 1 章划线与批注AI 陪读

Chapter 1 Building Abstractions with Procedures

“实际上,我们是用咒语召唤计算机的精灵。”

“In effect, we conjure the spirits of the computer with our spells.”

查看原文 →

一本以巫术而不是硬件开篇的教科书——这个比喻就是一个主张:编程是心灵的行为,不是机器的行为。

Chapter 1 Building Abstractions with Procedures

“像设计良好的汽车或核反应堆那样,是以模块化的方式设计的”

“like well-designed automobiles or nuclear reactors, are designed in a modular manner”

查看原文 →

咒语的说法被悄悄换成了安全工程。三页里两种口气——而整本书就活在它们之间的缝里。

Chapter 1 Building Abstractions with Procedures

“Lisp 不是一次协调一致的设计努力的产物。”

“Lisp was not the product of a concerted design effort.”

查看原文 →

引人注目的是,他们居然以此自夸。他们将要教的这份优雅是长出来的,不是规划出来的——这是对当时那些委员会设计的语言的一个狡黠的论证。

另外 17 条批注
Chapter 1 Building Abstractions with Procedures

“用 Lisp 编程非常好玩。”

“programming in Lisp is great fun.”

查看原文 →

在一页关于效率和主流地位的防御性论证之后,真正的理由最后、也最平白地到来了。诚实。

§1.1 The Elements of Programming

“让我们人类感到困惑的,是像下面这样仍然相对简单的表达式”

“It is we humans who get confused by still relatively simple expressions such as”

查看原文 →

极限不在机器。缩进在这里被点名为人类工作记忆的义肢,而不是风格规则。

§1.1 The Elements of Programming

“它所起的作用,就像代词在自然语言里所起的作用”

“which plays the same role that a pronoun plays in natural language”

查看原文 →

我所知道的对形式参数最好的一句解释——绑定,用每个读者已经能流畅做到的事来说明。

§1.1 The Elements of Programming

“单看 sum-of-squares 的定义,是看不出来的”

“one could not tell by looking at the definition of sum-of-squares”

查看原文 →

无从区分就是回报:抽象恰恰在你看不穿它的时候起作用。基本操作从来只是一个起点的约定。

§1.1 The Elements of Programming

“并不是要描述解释器真正怎样工作”

“not to provide a description of how the interpreter really works”

查看原文 →

教科书很少递给你一个模型,同时就标上它是假的。靠预定的淘汰来教学。

§1.1 The Elements of Programming

“正则序求值可以是一个极有价值的工具”

“normal-order evaluation can be an extremely valuable tool”

查看原文 →

他们以效率为由否决它,然后悄悄承认以后是不可或缺的。全书方法的缩影:没有东西被抛弃,只是被推迟。

§1.1 The Elements of Programming

“如果没有一个 <p> 被发现为真,这个 cond 的值就是未定义的。”

“If none of the <p>'s is found to be true, the value of the cond is undefined.”

查看原文 →

注意第一个 abs 没有 else——三个子句碰巧覆盖了一切。一个有意脆弱的例子,放在他们展示更安全的版本之前。

§1.1 The Elements of Programming

“注意 and 和 or 是特殊形式,不是过程”

“Notice that and and or are special forms, not procedures”

查看原文 →

短路不是优化,而是语义。求值参数的代价,已经成了含义的一部分——这正是习题1.5的主题。

§1.1 The Elements of Programming

“过程必须是有效的。”

“Procedures must be effective.”

查看原文 →

三个词,承载了整章。数学可以指,代码必须走。

§1.1 The Elements of Programming

“这只是在回避问题。”

“This only begs the question.”

查看原文 →

可爱的干巴巴的讽刺——给一个定义裹上括号,并不会让它变得可计算。Lisp 的语法不是 Lisp 的实质。

§1.1 The Elements of Programming

“说明了迭代可以不用任何特殊结构来实现”

“demonstrates how iteration can be accomplished using no special construct”

查看原文 →

安静的夸耀:没有循环关键字,却有 C 的表达范围。重复从来不是一种控制结构,只是自指。

§1.1 The Elements of Programming

“平方是怎么算出来的这些细节,可以被抑制”

“The details of how the square is computed can be suppressed”

查看原文 →

抽象被定义为获准的遗忘。一个程序规模的极限,就是你一次必须记在脑子里的东西的极限。

§1.1 The Elements of Programming

“如果我们把 guess 改名为 abs,就会因为捕获变量 abs 而引入一个错误”

“If we renamed guess to abs we would have introduced a bug by capturing the variable abs”

查看原文 →

这个括号,正是不言自明的原则开始咬人的地方——只有在你按名字知道自己依赖的一切时,改名才是安全的。自由变量是一种未登记的依赖。

§1.1 The Elements of Programming

“其他过程(sqrt-iter、good-enough? 和 improve)只会塞满他们的脑子。”

“The other procedures (sqrt-iter, good-enough?, and improve) only clutter up their minds.”

查看原文 →

命名是一种稀缺的公共资源——你的辅助函数在悄悄地向其他每个程序员征税。支持作用域的论证,先是社会的,然后才是技术的。

§1.1 The Elements of Programming

“不必把 x 显式地传给其中每一个过程”

“it is not necessary to pass x explicitly to each of these procedures”

查看原文 →

优雅,但留意换掉了什么:那些辅助过程原本是可移植的黑盒,现在被封进了 sqrt 的上下文。用复用换了简洁。

§1.1 The Elements of Programming

“的确,在逻辑学和程序设计语义学的文献里,有一段很长的关于代换的错误定义的历史。”

“Indeed, there is a long history of erroneous definitions of substitution in the literature of logic and programming semantics.”

查看原文 →

一条脚注悄悄承认,这本书核心的那个“简单”模型,是专家们栽过跟头的雷区。教学上的诚实,兼作一个警告。

§1.1 The Elements of Programming

“著名的逻辑学家在这里犯过令人难堪的错误。”

“Famous logicians have made embarrassing errors here.”

查看原文 →

现在说了两遍,且带着兴味。作者们要学生感到:严格并不居高临下——最聪明的人恰恰在这里失过足。

读后感AI 陪读

悖论:抽象恰恰在你看不穿它的时候成功

第1章沿着一条长弧,从咒语走向工程,而它开头的两种口气——召唤计算机精灵的咒语,然后是“像设计良好的汽车或核反应堆”那样模块化建造的程序——不是文风上的失误,而是这一章把论题说了两遍。编程是心灵的行为;心灵很小;所以此后的一切,都是节省注意力的机器。这样读,这一章看似杂乱的内容(命名、递归、增长率、素性、lambda),就收束成一门课程:构造出内部你被允许忘掉的结构。

开头的框架——基本表达式、组合的手段、抽象的手段——看似一份整齐的分类,但它真正的作用,是让这三种机制成为递归的、没有尽头的。一旦 sum-of-squares 与基本操作无从区分,“基本”这个范畴就被揭穿为一个“你碰巧从哪里开始”的约定。这一章第一个真正的思想动作就是这次坍缩:抽象恰恰在你看不穿它的时候成功,而作者们坦率地说,回报是认知上的,而不是数学上的。缩进的引入,不是作为风格,而是作为一种义肢,因为“是我们人类会感到困惑”。形式参数被解释成一个代词——这是我知道的对绑定最好的一句话说明,用每个读者已有的能力,解释一个形式化装置。程序的极限被直截了当地点明,就是你一次必须记在脑子里的东西的极限。

继续阅读

然后是代换模型,以及这一章最不同寻常的教学姿态:一个模型,递过来的同时就被标上“假”——“并不是要描述解释器真正怎样工作”。靠预定的淘汰来教学。脚注加倍地强调,坦白关于代换有一段很长的错误定义的历史,著名的逻辑学家在这里犯过令人难堪的错误,说了两遍,且分明带着兴味。这不是谦虚的表演。这是在主张:严格并不居高临下,你觉得自己蠢的地方,正是专家们栽跟头的地方,而一条看上去正确的简单规则,可能是雷区。应用序对正则序,也是同样处理:先以效率为由否决,然后悄悄承认以后是不可或缺的。这本书里没有东西被抛弃;东西只是被推迟。这就是方法,微缩在这里清晰可见。

条件那一节藏着比它所宣称的更尖锐的一点。第一个 abs 是用三个 cond 子句写的,它们只是碰巧覆盖了所有情形,紧接在课文指出没被覆盖的 cond 是未定义之后——一个有意脆弱的例子,放在更安全的那个之前。而 and 和 or 被点明是特殊形式,不是过程,这是这一章不再只谈方便的地方:短路意味着求值一个参数的代价,已经进入了表达式的含义。一旦如此,你就不能仅凭一个程序的形状来推理它,而教训是:求值顺序是语义,不是优化。

牛顿法值得它的篇幅,因为它把知识劈成两半——说明式的(平方根是什么)对命令式的(怎么得到一个)——而那个三个词的裁决,“过程必须是有效的”,承载了整章。数学可以指,代码必须走。在一个裹着括号的定义旁,那句干巴巴的“这只是在回避问题”是同一把刀:Lisp 的语法不是 Lisp 的实质,而一个只是重述问题的递归规范,什么也计算不出来。同样,“迭代可以不用任何特殊结构来实现”是一句值得一听的夸耀——重复从来不是一种控制结构,只是从远处看到的自指。

块结构和词法作用域,是我最想让读者抵制这一章自身热情的地方。支持作用域的论证,是先在社会层面、后在技术层面提出的:你的辅助过程“只会塞满他们的脑子”,命名被当作一种稀缺的公共资源,每个私人过程都在悄悄地向它征税。有理。但第二步,让 x 保持自由,这样辅助过程就不必传它,换掉了一些真实的东西。那些辅助过程原本是可移植的黑盒;现在它们被封进了 sqrt 的上下文里。而关于把 guess 改名为 abs 就会捕获变量的那个括号,正是这条不言自明的原则咬人的地方:自由变量是一种未登记的依赖,所以只有在你已经按名字知道自己依赖的一切时,改名才是安全的。这一章把词法作用域说成纯粹的收益。它是一种权衡——用复用换简洁,又用一些易读性同时换这两样。

效率方面的材料(树形递归的斐波那契、换零钱、逐次平方、欧几里得)常被读作算法上的插曲。并不是。它是这一章对自己的反驳:抽象让你能像写一个线性过程那样顺手地写出一个指数过程,记号里没有任何东西警告你。增长的阶被引入,当作缺失的感觉器官——是看穿你被告知要忘掉的那层抽象的唯一办法。费马检验把这推到边缘,用确定性换速度,到达概率性算法,答案正确的概率是你选的,而卡迈克尔数则提醒我们:检验的保证是关于检验的,不是关于那个数的。

收尾的运动——sum 推广成积分,lambda 和 let 溶解了有名字的辅助过程,半区间搜索和不动点作为一般方法,average-damp 和 deriv 返回过程,牛顿法被重建为不动点,两种平方根被 fixed-point-of-transform 统一起来——是这一章在偿还它自己开头的前提。论证了抽象是获准的遗忘之后,它表明值得忘掉的,通常是方法而不是数据。最后给一等公民地位的四项权利命名,则揭示:前面的六十页,是对一个大多数读者从未注意到其存在的限制,确实是一种限制的演示。

我想对读者说的是:这一章很容易被误当作 Scheme 入门,而这样读它,回报很差。它的习题才是论证真正落地的地方,它的脚注承载了它一些最好的思考。把对自身会出错的承认读成指示,而不是道歉——递给你的模型在设计上就是暂时的,你是要超越它的。留意文本推销一项技术而略去代价的地方;这样的地方是有的,词法作用域首当其冲,而注意到它们,才是这一章真正在教的技能。还要认真对待这一点:这一切诚实的理由是最后、也最平白地到来的,在关于效率和主流地位的整整一页防御性论证之后:用 Lisp 编程非常好玩。一本这样严谨的书承认这个,不是口气上的失误。这是对“当初为什么有人要用咒语召唤精灵”的最终回答。

在 FeedLitra 里读第 1 章,每段旁边都有这些卡片。

开始读书

论证在哪里吃力

不是反驳,而是细心的读者会想追问的地方。

  1. 本书把形式化正确性当作无法扩展而放弃,却又坚持数据表示要按所述条件来验证,让人不清楚到底还留下多少形式化的纪律。

    “比什么都重要的是,发现并掌握强大的组织技术,能加快我们创造大型而有意义的程序的能力。”

    “More than anything else, the uncovering and mastery of powerful organizational techniques accelerates our ability to create large, significant programs.”

    Foreword查看原文 →
  2. 给人类读者写程序,可能与忠实地给世界建模相冲突,因为镜像的策略接纳了有状态的对象,而读者更难对它们作推理。

    “首先,我们想确立这样一个观念:计算机语言不只是让计算机执行操作的一种方式,而是一种新颖的、表达方法论思想的形式化媒介。”

    “First, we want to establish the idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology.”

    Preface to the First Edition查看原文 →
  3. 本书既把元循环求值器颂扬为编程中最根本的想法,又宣布它在控制上的解释是破产的,让人不清楚一个自我描述的求值器到底解释了多少。

    “求值器,也就是决定编程语言中表达式含义的东西,不过是另一个程序。”

    “The evaluator, which determines the meaning of expressions in a programming language, is just another program.”

    Chapter 4 Metalinguistic Abstraction查看原文 →
  4. 允许复杂的操作作为基本操作自由存在,与“下降到 Lisp 之下”别扭地并存——后者恰恰因为借来的机制把要紧的机制藏了起来。

    “我们总是可以把复杂的“基本操作”换成更简单的基本操作。”

    “We can always replace the complex ``primitives'' by simpler primitive operations.”

    §5.1 Designing Register Machines查看原文 →
另外 2 处张力,以及 8 条证据上的局限
  1. 如果每个像样的程序已经是某种语言的求值器,那么“不断迁移到新语言”的劝告,就有变得无法证伪、而不是真正的设计选择的风险。

    “然而,随着我们面对越来越复杂的问题,我们会发现,Lisp,或者说任何固定的编程语言,都不足以满足我们的需要。”

    “However, as we confront increasingly complex problems, we will find that Lisp, or indeed any fixed programming language, is not sufficient for our needs.”

    Chapter 4 Metalinguistic Abstraction查看原文 →
  2. 抽象同时向上追到语言设计、向下追到数据通路,而本书并没有讲定,对一个具体问题,从业者该走哪个方向。

    “然而,随着我们面对越来越复杂的问题,我们会发现,Lisp,或者说任何固定的编程语言,都不足以满足我们的需要。”

    “However, as we confront increasingly complex problems, we will find that Lisp, or indeed any fixed programming language, is not sufficient for our needs.”

    Chapter 4 Metalinguistic Abstraction查看原文 →

证据上的局限

  1. 反对数据结构泛滥的理由,是当作语言社群之间的文化对比来论证的,而不是通过任何实测比较。

    “让 100 个函数操作一种数据结构,比让 10 个函数操作 10 种数据结构要好。”

    “It is better to have 100 functions operate on one data structure than to have 10 functions operate on 10 data structures.”

    Foreword查看原文 →
  2. 教学上的证据,来自一所学校的必修入门课,所以关于初学者能学会什么的主张,建立在单一的教学环境上。

    “第二,我们认为,在这个层次上,一门课程所要讲授的基本内容,不是某种编程语言结构的语法,也不是为高效计算特定函数而设计的巧妙算法,甚至不是对算法的数学分析和计算的基础,而是用来控制大型软件系统智力复杂性的那些技术。”

    “Second, we believe that the essential material to be addressed by a subject at this level is not the syntax of particular programming-language constructs, nor clever algorithms for computing particular functions efficiently, nor even the mathematical analysis of algorithms and the foundations of computing, but rather the techniques used to control the intellectual complexity of large software systems.”

    Preface to the First Edition查看原文 →
  3. 关于抽象带来维护好处的主张,靠的是小型的演算例子,比如对有理数构造函数的一处局部修改,而不是大型系统的数据。

    “把对表示的依赖限制在少数几个接口过程里,既帮助我们设计程序,也帮助我们修改程序,因为这让我们保有考虑别的实现方案的灵活性。”

    “Constraining the dependence on the representation to a few interface procedures helps us design programs as well as modify them, because it allows us to maintain the flexibility to consider alternate implementations.”

    §2.1 Introduction to Data Abstraction查看原文 →
  4. 仅仅 define 是一种有限的抽象手段,所以三种机制的说法,要等到引入过程定义之后才算完全站得住。

    “每一种强大的语言,都有三种机制来完成这件事:”

    “Every powerful language has three mechanisms for accomplishing this:”

    §1.1 The Elements of Programming查看原文 →
  5. 赋值引进的那些难题——同一性、时间和并发——在所概括的材料里是被点名为后果,而没有被解决。

    “处理对象、变化和同一性的困难,是我们在计算模型里必须同时间打交道的一个根本后果。”

    “The difficulties of dealing with objects, change, and identity are a fundamental consequence of the need to grapple with time in our computational models.”

    Chapter 3 Modularity, Objects, and State查看原文 →
  6. 放弃固定语言的动机,依据的是一个跨领域的电路设计类比,而“像样的程序就是求值器”的证据,来自本书自己的三个案例,它们是因为本来就带有类似语言的结构才被选中的。

    “然而,随着我们面对越来越复杂的问题,我们会发现,Lisp,或者说任何固定的编程语言,都不足以满足我们的需要。”

    “However, as we confront increasingly complex problems, we will find that Lisp, or indeed any fixed programming language, is not sufficient for our needs.”

    Chapter 4 Metalinguistic Abstraction查看原文 →
  7. 正则序、非确定性和逻辑求值,是后面各节的预告,而不是这里所概括的材料中展示的成果。

    “把求值器作为 Lisp 程序提供出来的一个重要好处是:我们可以把别的求值规则描述成对求值器程序的修改,从而实现它们。”

    “An important advantage of making the evaluator accessible as a Lisp program is that we can implement alternative evaluation rules by describing these as modifications to the evaluator program.”

    Chapter 4 Metalinguistic Abstraction查看原文 →
  8. 寄存器机器和递归的论证,靠的是像 GCD 这样的小型示意机器,以及对控制器各点上寄存器状态的推理,而不是现实的设计或测量。

    “因此,我们将从硬件架构师的角度,而不是机器语言程序员的角度,来着手我们的任务。”

    “Thus, we will approach our task from the perspective of a hardware architect rather than that of a machine-language computer programmer.”

    Chapter 5 Computing with Register Machines查看原文 →

全书概念导图

5 组概念:通俗的叫法、一句话解释,以及原书自己的用语。

1 · 驯服复杂性,而不是证明
真正的技能:把想法保持得小到能放在脑子里
核心的工程技能:抽象、约定的接口和新的描述性语言,使大型系统在智力上可以管理。原书用语: Controlling intellectual complexity
知道一样东西,就是知道一步一步怎么做
从“怎么做”的角度来研究知识,对照数学“是什么”的角度。原书用语: Procedural epistemology
程序在机器内部引发的、看不见的活动
机器内部那个看不见但真实存在的实体,在程序的指引下,随着演化操作数据。原书用语: Computational process
2 · 语言只有三个部分
把一整块东西起个名字,这样就能把它当一个词来用
让复合元素可以被命名、并当作单一单元来处理的语言特性;控制复杂性的核心手段,先用于过程,再用于数据。原书用语: Means of abstraction
括号:一个运算符加上它的各部分,永远嵌套下去
一个带括号的应用,运算符在最左,按一条递归的一般规则求值,只有自带规则的表达式类型例外;有些可以通过翻译成别的类型派生出来。原书用语: Combination and special forms
把参数换进名字的位置,来算出一次调用
把过程应用理解为用实参对形式参数做文本替换;没有赋值时成立,有了赋值就失效。原书用语: Substitution model
解释器用来查找东西的、相互链接的名字表
解释器的“名字-对象”存储:一串把约束变量与值配对的框架,由内向外搜索,给每个变量一个可变的位置。原书用语: Environment of frames
3 · 由承诺来定义的数据
一对“造出来”与“取回来”的过程,必须信守承诺
一种表示之所以合法,是因为它的构造函数和选择函数满足所述条件,而不是因为背后有存储。原书用语: Data as conditions on constructors and selectors
少数几个过程筑成的一堵墙,隐藏数据是怎么存的
一道层次的边界,一小组接口过程把数据抽象的使用者同它的实现者隔开。原书用语: Abstraction barrier
一块有两个槽的粘合件,一切结构都由它做成
由 cons 构造、由 car/cdr 读取的唯一基本复合结构,可以实现为一个按要求返回其各部分的过程;它的闭包性质产生层次结构。原书用语: Pair and procedural representation of data
每一级传递的形状都相同,所以各部件可以卡在一起
约定每一级传递同一种结构,这样标准的、可输出的部件就能自由地混搭;统一性优于结构的泛滥。原书用语: Conventional interfaces
4 · 状态使对象能给变化建模
按照被建模的东西的分法,来划分程序
把程序构造得像被建模的系统那样分解,包括让数据去迎合世界,而不是让控制复杂化。原书用语: Modularity by mirroring the world
一个记得自己过去、因而回答会有所不同的对象
一个行为取决于其历史的对象,它的历史被概括在状态变量里,并且在各部分变化的同时保持同一性。原书用语: Local state and identity under change
一旦值可以变化,相等的东西就不再能互相交换
相等的东西可以互相代换而不改变值,同一个调用总返回同一个结果——这是不带赋值的程序设计所具有的性质,命令式的更新把它们放弃了。原书用语: Referential transparency and functional versus imperative style
5 · 运行程序的程序
别只写程序了;去设计你所需要的语言
从编写程序上升一层,去设计、构造程序所用的语言。原书用语: Metalinguistic abstraction
一个决定表达式含义的朴素程序
一个普通的、可以修改的过程,它给表达式以含义,办法是把“环境中的表达式”归约为“应用于实参的过程”,再归约回去;用它自己的语言写成时就是元循环的,喂给它一台机器的描述时就是一台通用机器。原书用语: Evaluator and the eval/apply cycle
下降到导线和单元:对几个有名字的槽位一步一步操作
一台一步一步地对有名字的单元执行指令的机器,寄存器和操作是数据通路,一个显式的控制器序列决定下一步运行什么;通过逐条指令的执行过程和一个 pc 寄存器来模拟。原书用语: Register machine with data paths and controller
让递归运转起来的,是一个存着返回位置的栈
一个 continue 寄存器记录一段共用的指令序列该从哪里继续,而一个后进先出的、存放已保存寄存器的存储,使一份机器副本可以服务每一个嵌套的或树形递归的调用。原书用语: Stack, subroutines and recursion

值得停下来的段落AI 陪读

几处值得停下来的原文,每处附一句它为什么对全书要紧。

Preface to the First Edition · Preface to the First Edition

“程序必须写给人读,只是顺带才给机器执行”

“programs must be written for people to read, and only incidentally for machines to execute”

查看原文 →

全书弧线的公理:如果程序是写给人看的散文,那么之后的每一种手段——抽象屏障、数据导向的分派、显式的寄存器机器——都是给读者的辅助,而不是对机器的优化。

第 4 章 · Foreword

“完整的形式化正确性论证很少伴随大型程序”

“complete formal arguments of correctness seldom accompany large programs”

查看原文 →

“证明无法扩展”这一承认,是五章全部的隐含前提:习惯用法、品味和组织技术,在正确性证明走到头的地方顶了上来。

第 2 章 · Chapter 2 Building Abstractions with Data

“我们忘掉了这些符号代表什么”

“we forget about what the symbols stand for”

查看原文 →

“忘掉”是统一全书的那个动词——代换模型、抽象屏障、类型塔和编译后的词法地址,都是获得许可的遗忘。

另外 9 条批注
第 3 章 · §3.1 Assignment and Local State

“注意,表达式 (withdraw 25) 求值两次,产生的值不同。”

“Observe that the expression (withdraw 25), evaluated twice, yields different values.”

查看原文 →

全书的支点:一个表达式求值两次,就结束了第1到2章的函数式天真,并迫使时间、同一性、流,最终是显式控制,进入故事。

第 4 章 · Chapter 4 Metalinguistic Abstraction

“求值器,也就是决定编程语言中表达式含义的东西,不过是另一个程序。”

“The evaluator, which determines the meaning of expressions in a programming language, is just another program.”

查看原文 →

从语言使用者到语言建造者的铰链,也是让第5章的寄存器机器成为必然、而不是附录的那句话。

第 5 章 · Chapter 5 Computing with Register Machines

“元循环求值器本身是一个 Lisp 程序,因而继承了底层 Lisp 系统的控制结构”

“the metacircular evaluator is itself a Lisp program and hence inherits the control structure of the underlying Lisp system”

查看原文 →

本书在审计自己:它点出了第4章欠下的债——用求值来解释求值——而这个坦白,是进入第5章唯一诚实的入口。

Preface to the First Edition · Preface to the First Edition

“他们应当知道不该读什么,以及在任何时刻不需要理解什么。”

“They should know what not to read, and what they need not understand at any moment.”

查看原文 →

获得许可的无知,是书里每一道抽象屏障在人这一侧的对应物——正是这种纪律,让分层的系统对一个小小的头脑真正可处理。

第 3 章 · Chapter 3 Modularity, Objects, and State

“我们组织一个大型程序的方式,是由我们对所要建模的系统的认识决定的”

“the way we organize a large program is dictated by our perception of the system to be modeled”

查看原文 →

“认识”承认了本书的架构建模的是我们的世界观,而不是世界本身——这就是为什么第3章提供两种相互竞争的策略,第4章为每个问题提供一种新语言。

第 2 章 · §2.1 Introduction to Data Abstraction

“单一的复合数据基本体——序对,由过程 cons、car 和 cdr 实现——是我们唯一需要的粘合剂。”

“The single compound-data primitive pair, implemented by the procedures cons, car, and cdr, is the only glue we need.”

查看原文 →

全书所押的一个朴素宣称:一个两个槽的盒子,必须承载序列、树、通用程序包、流,最后是解释器自己的数据,而它做到了。

第 1 章 · §1.1 The Elements of Programming

“让我们人类感到困惑的,是像下面这样仍然相对简单的表达式”

“It is we humans who get confused by still relatively simple expressions such as”

查看原文 →

受限的资源很早就被点名,且从未改变:不是时钟周期,也不是内存,而是人的工作记忆,书里的每一项技术都是针对这个约束而设计的。

第 4 章 · Chapter 4 Metalinguistic Abstraction

“计算机科学本身不过是(也不多于)构造恰当的描述性语言的学科”

“computer science itself becomes no more (and no less) than the discipline of constructing appropriate descriptive languages”

查看原文 →

本书花了五章去挣得的、对整个领域的重新定义——在这个框架下,连第5章下降到硬件,也读成了语言设计。

Acknowledgments · Acknowledgments

“应当避免控制上的复杂性,集中精力去组织数据”

“one should avoid complexities of control and concentrate on organizing the data”

查看原文 →

一句话的论题,预测了全书的重心:数据的组织高于控制上的花巧,从序对一路到数据导向的分派都信守这一点,只有第5章不得不让控制本身成为数据时才打破。

读后感AI 陪读

否认它自己的名字:不是科学,几乎与计算机无关

SICP 是一本书,只有一个主题:对智力复杂性的管理,一路追到管理的工具本身变成了主题。前面的部分直截了当地说明了这套方案——佩利的前言是一份宣言,其真正的论证是一次认识论上的撤退,只是装扮成了胜利(形式化证明无法扩展,所以编程必须成为一种由可信的习惯用法和组织技术构成的手艺传统),而序言把这个领域从工程挪到了方法的人文学科,否认了它自己的名字:不是科学,与计算机也几乎无关。致谢随后说明了这为什么站得住——这不是一本教科书,而是一种教学文化的自画像:祖先、趣味上的合作者构成的同心圆,以及十五年里点名的习题课老师。从这里起,脊梁攀一道梯子,每一级都建在上一级的天花板上。第1章把过程当作抽象的单位,并且在“给模式命名,而不只是实例化模式”这一愿望的驱动下,使过程成为一等公民——抽象本身成了可操作的对象。第2章对数据做同样的事,而外尔的题词给了它动词:忘掉。数据不是由存储定义的,而是由构造函数和选择函数所满足的条件定义的,所以表示可以被替换、被标记、被可加地分派、沿着一座塔被强制——直到多项式的最大公因子标出极限:类型抗拒唯一的排序。第3章是书有意失去天真的地方:用 set! 买来的状态,换来模块化并镜像世界,却失去代换模型、引用透明性和稳定的“相同”概念,把时间强行带进计算;流的回答是把整个历史变成数据,却需要显式的延时,拒绝与可变性混用,并在独立主体合并的那一刻把同步带了回来。哪种观点都不令人满意,而书这样说了。第4章通过改变问题来化解这个僵局:求值器是一个普通程序,所以当语言配不上问题时,就去写一个求值器——而一个小小的通用求值器,靠语法变换和数据抽象保持小巧,一次改几行,就改成了正则序、非确定性和逻辑程序设计。第5章随后偿还这种循环一直在欠下的债:元循环求值器在没有点名的情况下继承了宿主的控制结构,所以书下降到寄存器、栈、显式延续、带类型的指针、垃圾收集,以及一个编译自己的编译器。整条弧线上最尖锐的判断是:每一项表达力上的收益都是要付出代价的,而书总是点出代价——代换模型从第一章起就被说成是一种简化,串行化器仍会死锁,合一必须拒绝自指,封闭世界的“非”是一个有理由的缺陷,而编译带来的加速是靠测量证实、不是靠断言。第二尖锐的是结构上的:梯子的每一级都是同一个动作,即反身的转向,你一直在用的机制,变成了你现在去构造的对象——过程,数据,时间,语言,机器。对读者我想说:不要为 Scheme 读它,也不要被动地读。习题不是练习,而是论证缺失的中间部分,而各章对自身不足的坦白,正是思想真正活着的地方。这本书真正的礼物是一种思维习惯——对任何系统,问它让你忘掉什么,逼你记住什么,两者的差别要付出什么——而这种习惯,比它所教授的任何一门语言都活得更久。

关于作者:哈罗德·艾贝尔森

麻省理工学院电气工程与计算机科学系教授,SICP 合著者

  • 作为访问学者在谷歌任职,属于 App Inventor for Android 团队2
  • 《计算机程序的构造和解释》(即本书)1
  1. 计算机程序的构造和解释 - 维基百科
  2. 哈尔·艾贝尔森 - 维基百科

《计算机程序的构造和解释》是怎样被读的

6 位读者,以及他们各自拿这本书做了什么。

  1. 2000 · 翻译

    和田英一

    把第二版改写成日文,供课程使用

    SICP 日文版(2000)
  2. 2001 · 重写

    萨斯曼与怀兹德姆

    把本书的方法改写成经典力学

    SICM(MIT 出版社,2001)
  3. 2004 · 翻译

    裘宗燕

    把第二版译成中文

    SICP 中文版(2004)
  4. 2004 · 评论

    费莱森等

    主张用分阶段的设计秘诀取代这门课

    费莱森等,JFP 2004
  5. 2011 · 评论

    布赖恩·哈维

    在 MIT 停开 6.001 之后,为本书的持久价值辩护

    《为什么 SICP 很重要》(2011)
另外 1 位读者
  1. 2022 · 改编

    亨茨与里格斯塔德

    把全书正文和习题改写成 JavaScript

    SICP:JavaScript 版

网络书评怎么说

在这些读者书评里,SICP 被当作一部教的是编程、而不是 Scheme 的经典:评论者称赞它从第一性原理出发的整合式路径,称赞它那些大型的真实示例程序——解释器、编译器、电路模拟器——以及他们称为理解所必需的习题。有几位说,它改变了他们写代码的方式,同时承认一种常见的批评:对初学者来说太难。

综合已发表书评的看法;不展示评分,完整书评请点来源链接。

  1. 书评:《计算机程序的构造和解释》,哈罗德·艾贝尔森、杰拉尔德·杰伊·萨斯曼著
  2. SICP 书评
  3. SICP——结语
  4. 书评

来源与授权

CC BY-SA 4.0原书

  • 《计算机程序的构造和解释》(Structure and Interpretation of Computer Programs)第二版,作者 Harold Abelson、Gerald Jay Sussman,Julie Sussman 参与(© 1996 Massachusetts Institute of Technology),由 MIT 出版社以知识共享 署名-相同方式共享 4.0 国际许可协议(CC BY-SA 4.0)发布。免费阅读原书 · 许可证
  • 本页由 FeedLitra 撰写的文字(摘要、批注、读后感)按 CC BY-SA 4.0 授权。
  • 本页的引文中文译文属于原作的改编作品,按同一协议(CC BY-SA 4.0)授权。
  • 摘要、论点、批注和读后感由 FeedLitra 根据全书原文 AI 辅助生成,引文已对照原文核对。最近更新:2026 年 10 月 21 日。

常见问题

关于《计算机程序的构造和解释》,读者常问

应该读 SICP 的哪个版本?

一共三版。第一版(1984)出自 MIT 的入门课。第二版(1996)就是大家通常说的“SICP”:作者重新设计了解释器、寄存器机器模拟器和编译器,把示例改写成任何符合 IEEE 标准的 Scheme 都能运行,并补充了并发与非确定性计算。另有 Martin Henz、Tobias Wrigstad 与 Julie Sussman 改编的 JavaScript 版(MIT 出版社,2022),保留同样的五章结构。熟悉的语法对你有帮助就选 JavaScript 版,想读原文、配原版讲座就选 Scheme 版;两者都不是用来学这门语言本身的教程。

SICP 难读吗?

难在概念,不在数学。开头的例子只用到普通算术(开平方、最大公约数、素数检测);真正难的是递归、程序如何一步步求值,以及程序一旦能“记住”东西之后会发生什么变化。1984 年的序言说,当年 MIT 的学生大多没有受过正规的计算训练;1996 年的序言也承认,内容已经多到一个学期讲不完。不需要编程基础,但要准备好开着电脑边读边做习题。

应该按什么节奏读?

以下是我们的建议,不是作者的教学大纲:把大部分时间放在第 1–3 章,大约每周一到两节,并做完习题;第 4、5 章当作第二阶段再读。作者在序言里自己也说,他们有时会跳过逻辑程序设计(4.4 节),让学生使用寄存器机器模拟器但不讲它的实现(5.2 节),对编译器(5.5 节)只做简要介绍。你也可以这样安排。

读的时候有哪些配套资源?

MIT OpenCourseWare 上有 Abelson 和 Sussman 1986 年录制的 20 讲视频,以知识共享协议发布;它们对应第一版,但用来配第二版也合适。读 Scheme 版可以用自由软件 MIT/GNU Scheme,或 Racket 的 “#lang sicp” 包;读 JavaScript 版可以用 Source Academy 网站的交互环境。

读者通常卡在哪里?

常见的有五处。括号一开始看着别扭,过一两天就习惯了,例子最好每个都亲手敲一遍。1.2 节里,一个调用自身的过程仍然可以以循环方式执行,用小输入在纸上跟踪,直到分清“递归过程”和“递归计算过程”。3.2 节的环境图自己动手画比光看容易懂。第 4 章刚开始会觉得绕,4.1 节要慢慢读,用很小的表达式去跑求值器。最后,只有动手做点东西,这些想法才会留下来,所以每一节至少做几道习题。

在 FeedLitra 里一边读《计算机程序的构造和解释》,一边看这些卡片。

开始读书