
视频元信息
- 课程:MIT 6.001 Structure and Interpretation of Computer Programs(SICP)1986 传奇原版
- 讲次:第 4 讲(P04)——高阶过程(Higher-Order Procedures)
- 主讲:Hal Abelson
- 字幕:中英双语
- 时长:约 50 分钟(含课间休息)
本讲是 SICP 从「语言规则」迈向「编程惯用法」的关键转折点。核心思想是:过程(procedure)可以作为参数传入、也可以作为返回值产出,即「高阶过程」。这是全书后续所有抽象(如
map、filter、accumulate、数据导向编程)的基石。
一、开场与课程引入 [00:25]
视频开头约 6 分钟为课堂开场白,转录质量较差,内容大致是引入「抽象」与「优雅」的编程风格。
- 讲师提到:我们要让程序「更优雅」,慢慢逼近某种理想状态。
- 强调:编程的目标是让计算机为人服务,而不是人为计算机服务(computers to make people happy, not people to make computers happy)。
- 本讲要介绍的新概念是——高阶过程。
理解:这段开场虽然转录混乱,但透露了 SICP 的核心价值观——抽象的目的是让程序「更容易写、更容易读」,而非炫技。
二、从求和模式看「共同模式」 [06:30]
2.1 回顾已知程序:求和、平方和、π 近似
讲师先回顾了之前学过的几个程序:
- 整数求和:从 a 加到 b。
- 平方和:从 a 到 b 的平方累加。
- π/8 的级数近似:
1/(1×3) + 1/(5×7) + 1/(9×11) + ...
例子是 formula for finding pi over eight. It's a funny ugly mess. Something like one over one time three plus one over five times seven plus one over nine times eleven.
2.2 发现共同结构
讲师指出:这三个程序几乎一模一样,都是「一个求和(sum)」。
-
它们的差别只有两点:
- 步长不同:整数求和步长为 1,π 近似步长为 4。
- 被累加的项(term)不同:
- 整数求和:项就是 A 本身。
- 平方和:项是 A 的平方。
- π 近似:项是
1/(A×(A+2))。
-
其余部分(递归结构、终止条件、累加方式)完全相同。
Anytime we have a bunch of things like this that are identical, we're going to have to come up with some sort of abstraction to cover them.
2.3 语言规则 vs 编程惯用法
考点: 讲师在此区分了两个层次:
- 语言规则(rules of language):原语(primitives)、组合方法(means of combination)、抽象方法(means of abstraction)——这些你已经学过。
- 编程惯用法(common patterns of usage / idioms):这是学一门语言时真正要掌握的「套路」。
理解:这类似于自然语言学习——语法只是基础,真正让你流利的是「成语」和「固定搭配」。编程中的 idiom 就是那些一眼就能认出的常见模式。
易错点:很多人学编程只学语法,不学 idiom。结果遇到问题要从头推导,效率极低。SICP 的独特之处在于——你不仅知道这些 idiom,还能给它们命名。
三、抽象出通用求和过程 sum [09:57]
3.1 通用模式的形式化
讲师把上述三个程序的共同结构抽象成一个通用模式:
- 定义一个过程,接受两个参数:下界 a 和 上界 b。
- 若 a > b,返回 0。
- 否则,计算
(term a),加上「对(next a)到 b 递归调用本过程」的结果。
3.2 引入过程参数
考点: 关键洞察是——数字只是数据的一种,过程也可以是数据。
Numbers are just one kind of data. It seems to me perfectly reasonable to give all sorts of names to all kinds of data. For example procedure.
因此,sum 可以接受四个参数:
| 参数 | 含义 | 类型 |
|---|---|---|
term |
计算当前项的过程 | 过程 |
a |
当前索引(下界) | 数字 |
next |
计算下一个索引的过程 | 过程 |
b |
上界 | 数字 |
3.3 sum 的定义
(define (sum term a next b)
(if (> a b)
0
(+ (term a)
(sum term (next a) next b))))
要点说明:
term是一个过程:给定索引,返回该项的值。next是一个过程:给定索引,返回下一个索引。- 递归调用时,
term和next这两个过程本身也要作为参数传递下去,因为它们后面还要用。
易错点:递归调用中传的是
(next a)(next作用于a的结果),而不是next本身。同时next过程本身也要作为参数继续传递。这两个是不同的东西:
next= 计算下一个索引的过程(next a)= 用next作用于a得到的下一个索引值
3.4 缩进与美观打印
讲师特别提到:当参数写不下时,他会按参数对齐缩进,这样能一眼看出哪些参数是一组的。
思考:这是 Lisp 社区「pretty printing」惯例的体现。良好的缩进让程序可读性大幅提升,尤其在高阶过程中,过程参数和普通参数混在一起,缩进是唯一的视觉线索。
四、用 sum 重写三个程序 [12:30]
4.1 整数求和
需要一个恒等过程(identity procedure),因为 term 对整数不做任何变换:
(define (identity x) x)
(define (sum-integers a b)
(sum identity a (lambda (x) (+ x 1)) b))
要点:
identity作为term传入,对索引不做变换。(lambda (x) (+ x 1))作为next,实现步长 1。
4.2 平方和
(define (sum-squares a b)
(sum square a (lambda (x) (+ x 1)) b))
其中 square 是之前定义过的平方过程。这里 term 就是 square。
4.3 π 近似
(define (pi-sum a b)
(sum (lambda (i) (/ 1.0 (* i (+ i 2))))
a
(lambda (i) (+ i 4))
b))
要点:
term用匿名过程直接写:1/(i×(i+2))。next步长为 4。
4.4 关于匿名过程(lambda)
讲师说:I'm going to write the Greek lambda. The letter here. That is L.A.M.B.D.A.
lambda用于定义匿名过程(不需要命名的过程)。- 当某个过程只用一次时,没必要给它起名字,直接用
lambda写。 - 讲师开玩笑说:希望计算机产业能在键盘上加一个 λ 键。
理解:lambda 的引入让高阶过程写起来非常自然。没有它,每次传过程参数都要先 define 一个名字,代码会变得冗长。
五、为什么要抽象?——分解与封装 [16:00]
5.1 独立修改的能力
讲师问:为什么 sum 这么重要?
考点: 因为它把问题分解成两个独立的部分:
- 被累加的是什么(term)
- 如何累加(sum 的递归/迭代结构)
5.2 迭代 vs 递归实现
假设有人想换一种累加方式,比如迭代实现(可能比递归更高效):
- 如果原来三个程序各写各的(左黑板),那么改累加方式要改三处。
- 如果用
sum抽象(右黑板),只需改sum一处。
That decomposition allows me to independently change one part of the program and improve it perhaps without changing the other part.
思考:这就是「封装」思想的雏形。sum 把「如何做加法」封装起来,调用者只关心「加什么」。这与面向对象中的「策略模式」有异曲同工之妙。
六、学生提问:过程作为参数如何理解 [18:00]
6.1 问题:过程为什么能当参数传?
学生问:这里过程是怎么当作参数传递的?
讲师回答要点:
- 过程并不特殊。
square只是一个变量,它的值是一个过程。 - 过程可以被命名,因此可以被传递。
- 这里把
term过程作为参数传给sum,让它在递归中继续可用。 next也是作为参数传递。
6.2 替换模型(substitution model)
讲师建议用替换模型来理解:
- 调用
sum时,把实参替换到形参位置。 - 例如
(sum-integers 1 10)展开后,identity替换term,(lambda (x) (+ x 1))替换next。 - 然后逐步展开,就能看到
identity被应用、1+被应用。
易错点:
next作为参数传入时是过程本身;而(next a)是调用next得到的结果。看代码时要注意括号:
next→ 过程(next a)→ 值- 两者在递归调用中都要传,但用途不同。
七、课间休息与引入「不动点」 [21:00]
7.1 休息前的话
讲师说:计算机是让人快乐的,不是让人伺候计算机的。引入抽象的目的是让程序更好写、更好读。
7.2 回顾最复杂的程序:牛顿法求平方根
讲师展示之前学过的牛顿法求平方根程序,指出:
- 这个程序看起来复杂,一眼看不出它在算什么。
- 里面有
try循环、improve过程(做平均和除法)。 - 但真正的数学思想是什么?能不能用抽象把它讲清楚?
7.3 数学洞察:不动点
考点: 牛顿法求平方根的本质是寻找函数的不动点(fixed point)。
- 定义改进函数:
f(y) = (y + x/y) / 2 - 若
y = √x,则f(y) = (√x + x/√x)/2 = (√x + √x)/2 = √x - 所以 √x 是 f 的不动点——把 √x 代入 f,得到还是 √x。
不动点定义: 一个值,把它代入函数后,输出还是它自己。
推导:
f(√x) = (√x + x/√x) / 2
= (√x + √x) / 2 [因为 x/√x = √x]
= 2√x / 2
= √x
7.4 计算器类比
讲师举了一个生动例子:
- 把计算器调到弧度模式,反复按
cos键。 - 最终会收敛到约 0.739(cos 的不动点)。
- 这说明:某些函数可以通过反复迭代来找到不动点。
理解:这就是「迭代法」的直观演示。每次把上次的输出作为下次的输入,如果函数「收缩」,就会收敛到不动点。
八、定义 fixed-point 过程 [25:30]
8.1 用愿望思维写程序
讲师先「愿望思维」地写出平方根:
(define (sqrt x)
(fixed-point (lambda (y) (average y (/ x y)))
1.0))
- 先假设
fixed-point已经存在。 - 初始猜测为 1.0(对平方根来说,起点无所谓,有定理保证收敛)。
讲师说:This is called wishful thinking. I don't know how I'm going to make fixed point happen. But I'm going to write my program anyway.
考点: 「愿望思维」是 SICP 反复强调的编程方法——先假设辅助过程存在,把主程序写出来,再回头实现辅助过程。这是自顶向下设计的具体体现。
8.2 实现 fixed-point
(define (fixed-point f first-guess)
(define (close-enough? v1 v2)
(< (abs (- v1 v2)) tolerance))
(define (try guess)
(let ((next (f guess)))
(if (close-enough? guess next)
next
(try next))))
(try first-guess))
要点说明:
- 内部过程:
close-enough?和try都定义在fixed-point内部,外部不可见。 - 终止条件:当旧值和新值「足够接近」时停止。
- 迭代循环:
try不断把(f guess)作为新的 guess。 - tolerance:一个很小的容差常量(如 0.00001)。
易错点:
- 不要写成
(if (close-enough? guess next) guess (try next))——应该返回next,因为next是更新后的值。close-enough?用abs取绝对值,不能直接比较差值,否则负数差值会误判。
8.3 内部过程的意义
Internal loops are written by defining internal procedures.
理解:内部过程是 Lisp 中实现「循环」的惯用法。因为 Lisp 没有传统的 for/while,用递归的内部过程来实现迭代。同时,内部过程可以访问外部过程的变量(如 f、tolerance),形成闭包。
九、为什么用平均阻尼?——收敛性分析 [30:00]
9.1 一个更简单但失败的方法
讲师问:为什么不直接用 g(y) = x/y?它的不动点也是 √x,而且更简单。
答案:不收敛,会振荡。
举例:x = 2,初始猜测 1。
g(1) = 2/1 = 2g(2) = 2/2 = 1g(1) = 2g(2) = 1- ……永远在 1 和 2 之间振荡,永远不会接近 √2 ≈ 1.414。
9.2 平均阻尼(average damping)
考点: 解决办法是阻尼振荡——把新旧值取平均。
- 这就像信号处理中消除振荡的经典方法。
- 定义
average-damp:接受一个过程f,返回一个新过程,该过程对f(x)和x取平均。
(define (average-damp f)
(lambda (x) (average x (f x))))
9.3 用平均阻尼重写平方根
(define (sqrt x)
(fixed-point (average-damp (lambda (y) (/ x y)))
1.0))
要点:
average-damp接受过程(lambda (y) (/ x y)),返回一个新过程。- 新过程每次计算时,把
x/y和y取平均,从而阻尼振荡。 - 这样就能收敛到 √x。
推导:
g(y) = x/y (会振荡)
h(y) = (y + x/y)/2 (平均阻尼后,收敛)
易错点:
average-damp返回的是一个过程,不是数值。它的类型是「过程 → 过程」。这与sum的「过程 + 数值 → 数值」不同。理解这一点是理解高阶过程的关键。
十、学生提问:lambda 与多参数的区别 [36:00]
10.1 问题:能不能写成 (lambda (f x) ...)?
学生问:average-damp 能不能写成 (lambda (f x) (average (f x) x))?
讲师回答:不行。
(lambda (f x) ...)是一个接受两个参数的过程:一个过程f和一个数值x,返回一个数值。- 而
average-damp需要的是:接受一个过程f,返回一个过程。 - 返回的过程再接受
x,计算平均值。
类型对比:
| 写法 | 类型 |
|---|---|
(lambda (f x) ...) |
过程 × 数值 → 数值 |
(lambda (f) (lambda (x) ...)) |
过程 → 过程 |
易错点:
average-damp必须返回过程,因为它的返回值要放进fixed-point的过程槽位。如果返回数值,类型就不匹配了。
10.2 不用 lambda 的等价写法
讲师说:可以不用 lambda,但需要给内部过程起个名字:
(define (average-damp f)
(define (damped x)
(average x (f x)))
damped)
这与 lambda 写法完全等价。lambda 只是「匿名过程」的语法糖。
理解:lambda 的价值在于——当过程只用一次时,不必费心起名字。这让高阶过程的代码更简洁、更聚焦于逻辑本身。
十一、牛顿法(Newton's Method) [39:00]
11.1 牛顿法的一般形式
考点: 牛顿法是求函数零点(根)的通用方法。
- 目标:找到 y 使得
f(y) = 0。 - 迭代公式:
y_{n+1} = y_n - f(y_n) / f'(y_n)
其中 f' 是 f 的导数。
11.2 用牛顿法求平方根
要解 y² = x,即 y² - x = 0。
所以定义 f(y) = y² - x(注意:这里的 x 是外层传入的自由变量)。
(define (sqrt x)
(newton (lambda (y) (- (square y) x))
1.0))
要点:
(lambda (y) (- (square y) x))中,x是自由变量,由外层sqrt捕获。- 初始猜测 1.0。
11.3 实现 newton
(define (newton f guess)
(define df (derivative f))
(fixed-point (lambda (x) (- x (/ (f x) (df x))))
guess))
要点:
- 先求
f的导数df。 - 然后用
fixed-point迭代牛顿法公式。 - 这里再次体现「愿望思维」:
derivative还没实现,先假设它存在。
理解:牛顿法本质上是「不动点 + 特定迭代函数」的组合。fixed-point 是通用引擎,牛顿法只是给它喂了一个特定的迭代函数。
十二、实现 derivative [42:30]
12.1 导数的数值近似
(define (derivative f)
(lambda (x)
(/ (- (f (+ x dx)) (f x))
dx)))
其中 dx 是一个很小的数(如 0.00001)。
要点:
derivative接受过程f,返回一个过程(导函数)。- 返回的过程接受
x,用差分商近似导数。 - 这是「过程 → 过程」的又一例。
12.2 数值分析的警告
讲师说:The numerical analyst here should jump on me and say, don't do that. Computing derivatives produces noisy answers.
考点: 数值微分会产生噪声,实际工程中应谨慎使用。
- 差分商只是近似,
dx太小会因浮点误差而失真,太大则近似不准。 - 这里用它是为了教学清晰,展示抽象的力量。
思考:这体现了 SICP 的一贯风格——先讲清思想,再讨论工程细节。数值稳定性是后续课程(如 18.335)的内容。
十三、程序结构图:抽象机器 [44:30]
讲师展示了一张幻灯片,用方框和箭头描述整个程序的结构:
- sqrt 接受
x,构造出(lambda (y) (- (square y) x))。 - 这个过程的
x被「卷入」其中(通过闭包捕获)。 - 该过程作为
F传入 newton。 newton内部:F一部分直接使用,另一部分经过 derivative 变成DF。F和DF组合成迭代函数,送入 fixed-point。fixed-point配合初始猜测,产出 √x。
So what we've built is a very powerful engine, which allows us to make nice things like this.
理解:这张图是「数据流」视角的抽象机器。每个方框是一个过程,箭头是数据(包括过程本身)的流动。高阶过程的本质就是——过程在数据流中流动。
十四、第一类公民(First-Class Citizens) [47:00]
14.1 Christopher Strachey 的思想
讲师引用计算机科学先驱 Christopher Strachey(指称语义学创始人之一)的观点:
过程(函数)应当是编程语言中的第一类公民(first-class citizens)。
14.2 第一类公民的权利与特权
考点: 一个对象要成为第一类公民,必须满足:
- 可以被变量命名——如
(define square ...)。 - 可以作为参数传递给过程——如
sum的term参数。 - 可以作为过程的返回值——如
average-damp、derivative。 - 可以放入数据结构——如列表、向量(后续课程会讲)。
考点:这四条是判断「某对象是否第一类」的标准。在 SICP 中,过程是第一类公民,这是它区别于许多早期语言(如 Fortran、C)的关键特征。在 C 中函数不能返回函数,在 Java 早期版本中函数不是一等公民(需要包装成对象)。
14.3 第一类公民的意义
Having things like procedures as first-class data allows one to make powerful abstractions, which can code general methods like Newton's method in a very clear way.
- 第一类过程让通用方法(如牛顿法、不动点法)可以被清晰地编码。
- 这是「元编程」和「高阶抽象」的基础。
十五、学生提问:能否内联展开 [49:00]
15.1 问题:能不能把 derivative 直接写进 fixed-point?
学生问:能不能把 (derivative f) 直接放进 fixed-point 里?
讲师回答:可以,但有代价。
(define (newton f guess)
(fixed-point
(lambda (x) (- x (/ (f x) ((derivative f) x))))
guess))
要点:
- 语法上完全合法,因为求值规则是「先求值所有子表达式」。
- 但这样写有个坏处:每次迭代都会重新计算导数,效率低下。
- 原来的写法把
(derivative f)算一次,存为df,然后复用。
易错点:内联展开虽然语法正确,但可能导致重复计算。在性能敏感的代码中,应把不变的部分提取出来,只计算一次。这是「公共子表达式消除」的朴素版本。
15.2 求值规则回顾
讲师强调求值规则:
The evaluation rule is evaluate all of the parts of the combination, the operator, and the operands.
- 对组合式
(operator operand1 operand2 ...),先求值 operator,再求值各 operand。 - 所以
(derivative f)作为 operator 的一部分,会被求值——每次迭代都求一次。
思考:这个例子很好地说明了「语法正确」不等于「工程最优」。SICP 不仅教你怎么写对,还教你怎么写好。
十六、本讲总结
核心概念回顾
| 概念 | 说明 |
|---|---|
| 高阶过程 | 接受过程作为参数,或返回过程作为值的过程 |
| 过程作为参数 | sum 接受 term 和 next 两个过程参数 |
| 过程作为返回值 | average-damp、derivative 返回过程 |
| lambda | 匿名过程,用于一次性使用的过程 |
| 不动点 | f(y) = y 的解,可用迭代法求解 |
| 平均阻尼 | 消除振荡,让迭代收敛 |
| 牛顿法 | 求函数零点的通用方法,用导数迭代 |
| 愿望思维 | 先假设辅助过程存在,写出主程序,再实现辅助过程 |
| 第一类公民 | 可命名、可传参、可返回、可放入数据结构 |
关键代码汇总
; 通用求和
(define (sum term a next b)
(if (> a b)
0
(+ (term a) (sum term (next a) next b))))
; 不动点
(define (fixed-point f first-guess)
(define (close-enough? v1 v2)
(< (abs (- v1 v2)) tolerance))
(define (try guess)
(let ((next (f guess)))
(if (close-enough? guess next)
next
(try next))))
(try first-guess))
; 平均阻尼
(define (average-damp f)
(lambda (x) (average x (f x))))
; 平方根(平均阻尼 + 不动点)
(define (sqrt x)
(fixed-point (average-damp (lambda (y) (/ x y))) 1.0))
; 牛顿法
(define (newton f guess)
(define df (derivative f))
(fixed-point (lambda (x) (- x (/ (f x) (df x)))) guess))
; 导数
(define (derivative f)
(lambda (x) (/ (- (f (+ x dx)) (f x)) dx)))
本讲最核心的一句话:过程可以作为参数传入,也可以作为返回值产出——这就是高阶过程。它让「通用方法」可以被清晰地抽象和复用。
关键标签 / 关键词
SICP 高阶过程 higher-order procedure 过程作为参数 过程作为返回值 lambda 匿名过程 不动点 fixed-point 平均阻尼 average-damp 牛顿法 Newton's method 导数 derivative 愿望思维 wishful thinking 第一类公民 first-class citizen 抽象 abstraction 求和 sum MIT 6.001 Hal Abelson Christopher Strachey 闭包 替换模型 substitution model 数值微分 收敛性 振荡
生成时间:2026-10-11 13:52:39 生成平台:VideoNote www.xygy.top