视频封面

视频元信息

  • 课程名称: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 的全部认识,其实只有三条公理:

  1. (car (cons x y)) = x
  2. (cdr (cons x y)) = y
  3. cons、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