
视频元信息
- 课程名称:6.001 计算机程序的构造与解释(SICP)1986 原版
- 本讲主题:复合数据(Compound Data)
- 讲师:Hal Abelson(MIT)
- 说明:本讲是 SICP 第 4 讲,聚焦"数据抽象"与"复合数据",是全书最核心的方法论之一。
内容摘要(TL;DR)
本讲以有理数算术系统为例,引入"数据抽象"这一控制复杂度的核心方法论:通过构造器(constructor)与选择器(selector)建立抽象屏障,把"数据的使用"与"数据的表示"分离。随后引入 cons/car/cdr 作为构造复合数据的"胶水",并进一步用纯过程(lambda)实现 cons/car/cdr,揭示"数据"与"过程"边界的模糊性。
一、引入:为什么需要复合数据 [00:22]
本讲一开始承接上一讲"过程抽象"的思路,转向"数据抽象"。
- 上一讲通过
good-enough?等过程抽象,把"如何求平方根"的细节隐藏起来。 - 本讲要做的是类似的事情,但对象是数据:如何把"两个数"组织成一个"有理数"这样的概念实体。
- 核心目标:让程序语言能表达我们头脑中的概念(如有理数、点、线段),而不是被底层临时变量的细节淹没。
本讲主线:从"有理数算术"这个具体例子出发,引出数据抽象 → 抽象屏障 → 复合数据(pair)→ 用过程实现数据。
二、有理数算术:从公式到过程 [04:04]
2.1 分数运算的数学基础
讲师先回顾五、六年级的分数算术:
- 加法:
n1/d1 + n2/d2 = (n1*d2 + n2*d1) / (d1*d2) - 乘法:
n1/d1 * n2/d2 = (n1*n2) / (d1*d2)
考点:有理数加法的分子 = 第一个分数的分子 × 第二个分数的分母 + 第二个分数的分子 × 第一个分数的分母;分母 = 两个分母之积。
2.2 用"许愿式思维"定义过程
讲师采用与平方根相同的策略——wishful thinking(许愿式思维):
理解:先假设已经有了某个过程,再基于它写上层代码。这是 SICP 反复强调的自顶向下设计方法。
假设存在三个过程:
make-rat:接收分子和分母两个数,返回一个"云"(cloud,即有理数对象)。numerator:接收一个"云",返回其分子部分。denom:接收一个"云",返回其分母部分。
讲师用"cloud"这个词故意含糊——我们不关心有理数到底是什么,只关心它由这三个过程定义。
有了这三个过程,就可以写出:
+rat:接收两个有理数 x、y,返回make-rat( n(x)*d(y) + n(y)*d(x), d(x)*d(y) )*rat:接收两个有理数 x、y,返回make-rat( n(x)*n(y), d(x)*d(y) )
思考:这里 make-rat、numerator、denom 就是"构造器"和"选择器"的雏形,它们构成了后续要讲的"抽象屏障"。
三、为什么不能直接用四个数?[10:30]
有学生提问:"既然有分子分母,为什么还要包成 cloud?"
讲师用一段精彩的对比回答:
3.1 反例:用四个数字直接算
如果直接输入四个数(n1、d1、n2、d2),输出两个数(分子、分母),那么要计算 (x+y) * (s+t) 时:
- 需要一堆临时变量存放中间结果的分子分母。
- 这些临时变量是"有理数的内脏"(guts),会散落在整个系统里。
3.2 用有理数对象的优势
有了 +rat、*rat 后,表达式可以写成:
(*rat (+rat x y) (+rat s t))
理解:这段代码几乎就是数学表达式的直接翻译,程序语言因此能"表达我们头脑中的概念"。
考点:数据抽象的核心价值——让程序语言表达概念,而不是让程序员被底层表示细节淹没。
四、抽象屏障与数据抽象方法 [19:00]
讲师把有理数系统分成两层:
- 上层(使用层):
+rat、*rat、-rat、=rat等运算。 - 下层(表示层):
make-rat、numerator、denom的具体实现。
两层之间由构造器和选择器构成抽象屏障(abstraction barrier)。
考点:数据抽象的定义——通过假设构造器和选择器,把"数据对象的使用"与"数据对象的表示"隔离开来的编程方法论。
拓展:抽象屏障的好处是——只要遵守构造器/选择器的契约,下层实现可以随意更换,上层代码完全不受影响。
五、George 的问题:用 pair 实现有理数 [22:00]
现在轮到"George"来解决下层问题:如何真正把分子分母"粘"在一起?
答案是引入 list structure(表结构),具体是 pair(序对):
(cons x y):构造一个 pair,第一部分是 x,第二部分是 y。(car p):取出 pair 的第一部分。(cdr p):取出 pair 的第二部分。
5.1 盒指针表示法(box and pointer notation)
用方框和箭头画 pair:
[ * | * ]
| |
2 3
易错点:箭头的几何方向(上下左右)没有任何意义,只有连接关系有意义。很多人会误以为方向重要。
5.2 George 的实现
(define (make-rat n d) (cons n d))
(define (numerator x) (car x))
(define (denom x) (cdr x))
理解:这就是 pair 作为"数据胶水"的典型用法——把两个东西粘成一个整体。
六、测试与 bug:1/2 + 1/4 = 6/8? [30:00]
用刚实现的系统计算 1/2 + 1/4:
- 分子输出 6,分母输出 8,而不是期望的 3/4。
原因:+rat 直接套用分数加法公式,没有约分。
6.1 修复方案:在 make-rat 中约分
(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))
考点:约分位置的选择——可以在构造时约分,也可以在选择时约分,各有优劣。
思考:这就是数据抽象威力的体现——只需修改 make-rat 一处,上层 +rat 完全不用动。
七、另一种表示:延迟约分 [35:00]
讲师展示另一种实现:构造时不约分,在选择时约分。
(define (make-rat n d) (cons n d))
(define (numerator x) (/ (car x) (gcd (car x) (cdr x))))
(define (denom x) (/ (cdr x) (gcd (car x) (cdr x))))
7.1 哪种更好?
- 频繁构造、很少查看:延迟约分更好(省去构造时的 GCD 计算)。
- 频繁查看、很少构造:构造时约分更好。
考点:表示选择取决于使用模式——没有绝对优劣,只有适合场景。
7.2 设计哲学:推迟决策
讲师提出一个重要的设计原则:
"作为系统设计者,你被迫做决策;保持灵活性的方式是——在被迫之前,永远不要做决定。"
理解:数据抽象让我们给决策"起个名字"(如 make-rat),然后假装已经做了决定继续往下写,将来再回头面对真正的实现。
设计格言:推迟决策 ≠ 拖延。要在"做决定"和"拖延"之间找到那条极窄的界线。
八、let 与 define 的区别 [40:00]
学生提问:let 和 define 有什么区别?
define:在全局环境中建立名字,之后一直记得。let:建立局部上下文,其中的定义只在该上下文内有效。
(let ((z 10)) (+ z z)) ; => 20
z ; => 未绑定变量错误
易错点:
let中定义的名字不会污染外部环境,这是它与define最重要的区别。
九、数据抽象的威力:线段与点 [45:00]
讲师用"平面上的点与线段"再次演示数据抽象:
9.1 分层结构
- 最底层:数(numbers)
- 中间层:向量/点(vector/point),用 pair 表示
- 上层:线段(segment),用一对点表示
每层都有自己的构造器和选择器:
- 点:
make-vector、xcor、ycor - 线段:
make-segment、segment-start、segment-end
9.2 基于抽象的操作
- 中点:坐标取平均值
- 长度:
sqrt(dx² + dy²)
理解:这些操作只使用选择器,完全不关心底层如何用 pair 表示。
9.3 反面教材:不用抽象的代码
如果直接写 (car (car s))、(cdr (cdr s)) 这样的代码:
- 可读性极差
- 一旦表示改变(如 x、y 顺序调换),所有代码都要改
- 层数越多,复杂度爆炸
易错点:不要"漏出内脏"(let the guts hang out)——直接操作底层表示会让系统迅速失控。
十、闭包性质(Closure) [50:00]
讲师引入一个关键概念:
闭包(closure):如果一种组合手段能把东西组合起来,而组合的结果又能用同样的手段继续组合,就说这种组合手段是封闭的。
- pair 是封闭的:可以构造 pair of pairs。
- Fortran 的数组不是封闭的:可以构造数组 of numbers,但不能构造数组 of arrays。
考点:闭包是判断组合手段质量的标准——封闭的组合手段才能构建任意复杂的结构。
拓展:Scheme 的 list 之所以强大,正是因为 cons 的闭包性——任何复杂的数据结构都能用 pair 嵌套表达。
十一、什么是"数据"?用过程实现 pair [1:00:00]
这是本讲最深刻的部分。讲师先回顾:我们之前对 pair 的全部认识,其实只有三条公理:
(car (cons x y)) = x(cdr (cons x y)) = ycons、car、cdr是过程
11.1 用 lambda 实现 cons/car/cdr
(define (cons x y)
(lambda (pick)
(if (= pick 1) x y)))
(define (car p) (p 1))
(define (cdr p) (p 2))
验证:(car (cons 37 49))
cons返回一个过程,其中 x=37、y=49 被替换。car把该过程应用到 1,返回 37。cdr把该过程应用到 2,返回 49。
考点:数据可以用过程实现——只要满足公理,就是合法的 pair 表示。
11.2 哲学冲击:数据与过程的边界模糊
- 传统观念:数据是"名词",过程是"动词"。
- 本讲揭示:数据可以完全用过程表示,两者边界模糊。
思考:这是 SICP 最著名的"顿悟时刻"之一,也是后续"消息传递"、"面向对象"等范式的思想源头。
十二、学生提问:过程作为对象 [1:10:00]
学生问:"如果有两个 cons,系统怎么知道用哪个 x 和 y?"
讲师用"平方根 5 和平方根 20"作类比:
- 你不会担心
sqrt(5)和sqrt(20)会混淆。 - 同理,
(cons 37 49)和(cons 1 2)是两个独立的过程对象,各自"记住"了自己的值。
考点:过程是对象(procedures are objects)——过程不只是"动作",而是有独立存在的概念实体。
拓展:这引出一个深刻问题——(cons 37 49) 两次调用,是"同一个对象"还是"不同对象"?讲师说这是课程中段一整讲要讨论的问题(涉及 eq?、引用透明性等)。
关键标签 / 关键词
SICP #数据抽象 #抽象屏障 #复合数据 #pair #cons #car #cdr #构造器 #选择器 #有理数算术 #闭包性质 #过程即对象 #wishful-thinking #let与define #MIT6.001 #Scheme #lambda #box-and-pointer #延迟决策
生成时间:2026-10-11 13:55:19 生成平台:VideoNote www.xygy.top