
视频元信息
- 课程:MIT 6.001 Structure and Interpretation of Computer Programs(SICP)1986 年原版
- 讲次:第 9 讲(p09)——通用操作符(Generic Operators)
- 主讲:Hal Abelson
- 主题:抽象屏障、类型化数据、类型分派、数据导向编程、通用算术系统、强制类型转换(Coercion)
本节核心是解决"同一抽象接口存在多种不兼容表示"的系统组织问题。理解顺序:水平抽象屏障 → 垂直抽象屏障 → 类型化数据 → 类型分派 → 数据导向编程(表格)→ 通用算术系统 → 递归类型塔 → 强制类型转换。
一、从水平屏障到垂直屏障 [00:20]
前面课程讲的是"把使用与表示分离"(separating use from representation),即在抽象数据之上建立水平抽象屏障:上层用 real-part、magnitude 等抽象选择器,下层由某个人(如 George)负责具体实现。
理解:水平屏障解决的是"一个接口、一个实现"的问题,上层不需要知道数据长什么样。
但现实系统里往往同一种抽象存在多个互不兼容的实现。这时只靠水平屏障不够,还需要垂直屏障——让 George 的实现和 Martha 的实现各自独立存在、互不干扰。
类比:一家大公司有多个部门,每个部门对"人事记录"(personnel record)的存储格式约定完全不同。公司总部希望在任何地方都能问"这条人事记录的姓名是什么""职务描述是什么",而不必关心是哪个部门、用什么格式存的。
易错点: 常见的错误做法是"下发一份统一格式命令,要求所有部门改成兼容格式"——这在实际中几乎从不成功。正确做法是保留各部门的独立格式,在中间加一层通用操作符(generic operator)。
考点:generic operator 的定义——它的具体行为取决于它所面对的数据类型。并且系统应设计成:新部门加入时,不需要修改已有的任何代码。
二、用复数算术作为具体问题 [05:14]
为了把问题讲清楚,Abelson 选了一个比人事记录更自包含(self-contained)的问题:复数算术系统。
复数的两种表示
复数可以看成平面上的一个点 z,有两种等价表示:
- 直角坐标形式(rectangular form):
z = x + yi,x 为实部,y 为虚部。 - 极坐标形式(polar form):
z = r·(cos A + i·sin A),r 为模(magnitude),A 为辐角(angle)。
两者的换算关系:
x = r·cos(A),y = r·sin(A)r = √(x² + y²),A = arctan(y/x)
【拓展】复数的极坐标形式之所以重要,是因为乘除法在极坐标下极其简洁:模相乘、角相加。这正是后面"哪种表示更好"讨论的数学基础。
复数运算的数学公式
- 加法:实部相加、虚部相加。
Re(z1+z2) = Re(z1)+Re(z2),Im(z1+z2) = Im(z1)+Im(z2)。 - 乘法(极坐标下最自然):模相乘、角相加。
|z1·z2| = |z1|·|z2|,∠(z1·z2) = ∠z1 + ∠z2。 - 减法:与加法同构,对应部分相减。
- 除法:模相除、角相减。
考点: 加法在直角坐标下简单,乘法在极坐标下简单——这是后面"表示之争"的根本原因。
三、复数的抽象数据实现 [08:40]
与上一讲的有理数完全同构,先假定一组构造器和选择器:
构造器(constructors):
make-rectangular(real, imag):由实部、虚部构造复数make-polar(magnitude, angle):由模、角构造复数
选择器(selectors):
real-part(z)、imag-part(z)、magnitude(z)、angle(z)
在这层抽象之上,直接用公式实现运算:
add-complex:用real-part与imag-part取出两部分,分别相加,再用make-rectangular装回去。sub-complex:同上,但改为相减。mul-complex:用magnitude与angle取出,模相乘、角相加,再用make-polar装回。div-complex:模相除、角相减。
推导:注意 add-complex 用的是直角坐标的选择器,而 mul-complex 用的是极坐标的选择器——运算层并不关心底层是哪种表示,这正体现了抽象屏障的价值。
四、George 与 Martha 的两种表示 [12:30]
George 的方案:直角坐标表示
- 复数 = 一对
(real-part . imag-part) make-rectangular:直接cons成对real-part:取car;imag-part:取cdrmagnitude:√(car² + cdr²)angle:arctan(cdr / car)make-polar:先算出实部r·cos(A)和虚部r·sin(A),再cons成对
Martha 的方案:极坐标表示
- 复数 = 一对
(magnitude . angle) make-polar:直接cons成对magnitude:取car;angle:取cdrreal-part:r·cos(A),即car·cos(cdr)imag-part:r·sin(A),即car·sin(cdr)make-rectangular:先算√(x²+y²)和arctan(y/x),再cons成对
哪种更好?
- 如果系统大量做加法,George 的直角坐标表示更优(实部虚部直接相加)。
- 如果大量做乘除法,Martha 的极坐标表示更优(模乘角加)。
- 关键结论:你无法决定。 也许两种都得留着。
这正是需要垂直屏障的场景:George 的矩形复数与 Martha 的极坐标复数共存于同一系统,互不干扰,而上层的
add、sub、mul、div照常工作。
五、类型化数据(Typed Data)[18:50]
核心思想:让数据对象自带标签,标明它是谁造的。
- 不再只有普通的复数,而是"带签名的"复数:一个标签写着 "Martha",另一个写着 "George"。
- 每次操作前先读标签,就知道该用谁的规则处理。
类型化数据的构造:
attach-type(type, contents):用cons把类型符号和内容配对type(datum):取carcontents(datum):取cdr
类型谓词(type predicates):
- 判断是否为矩形复数:检查
type是不是符号rectangular - 判断是否为极坐标复数:检查
type是不是符号polar
易错点: 类型符号是符号(symbol),不是字符串,比较时要用符号相等判断。
六、让 George 和 Martha 接入系统 [23:00]
两人各自独立设计自己的复数表示包,接入系统只需很小的改动:
George 需要做的:
- 构造复数时,除了
cons成对,还要贴上类型标签rectangular。 - 重命名过程,避免与 Martha 冲突:
real-part→real-part-rectangularimag-part→imag-part-rectangularmagnitude→magnitude-rectangularangle→angle-rectangular
Martha 需要做的:
- 构造复数时贴上类型标签
polar。 - 重命名过程:
real-part-polar、imag-part-polar、magnitude-polar、angle-polar。
思考: 重命名这一步很烦人,但本节先跳过这个问题——后面讲"名字与环境的结构"时会给出把命名空间分开打包的办法。
七、类型分派(Dispatch on Type)[26:00]
现在需要一个"经理"(manager)来查看数据类型并分派。
通用选择器 real-part 的工作方式:
- 看这个复数的类型。
- 如果是
rectangular,剥掉类型(用contents),把内容交给real-part-rectangular。 - 如果是
polar,剥掉类型,交给real-part-polar。
imag-part、magnitude、angle 同理。
系统三部分结构:
- George 的矩形表示包
- Martha 的极坐标表示包
- 经理(manager):负责看类型、分派
具体例子
复数 1 + 2i:
- 在系统里表示为:
(rectangular . (1 . 2)),即内容(1 . 2)加上标签rectangular。 - 取
real-part时,经理看到是 George 的,剥掉类型,把(1 . 2)交给 George 的过程。
关键点: 同样的
(1 . 2)在 Martha 的世界里含义完全不同(模为 1、角为 2)。但系统不会混淆,因为任何一对数据从底层返回上层时,都会被贴上对应的类型标签——rectangular或polar。
考点:这种策略叫 "dispatch on type"(按类型分派)。
八、类型分派的缺陷:官僚瓶颈 [32:30]
批评一:命名冲突问题。 George 和 Martha 必须重命名各自的过程,很烦人。(本节暂不解决。)
批评二(重点):新人加入时的改动问题。
- George 和 Martha 都不受影响,各自待在自己的世界里。
- 但经理必须改:如果 Harry 带来一种新的复数类型,经理的所有过程里都要加上对 Harry 类型的判断。
核心痛点: 系统中最不灵活、阻碍变化的地方,恰恰是那个什么都不做的官僚(manager)——它只是转发的"纸面工作者"(paper pusher)。
九、表格与数据导向编程 [36:00]
抽象地看,经理实际上就是在查一张表(table):
| 操作 \ 类型 | polar | rectangular | ... |
|---|---|---|---|
| real-part | real-part-polar |
real-part-rectangular |
|
| imag-part | imag-part-polar |
imag-part-rectangular |
|
| magnitude | magnitude-polar |
magnitude-rectangular |
|
| angle | angle-polar |
angle-rectangular |
解决办法:把经理自动化掉(automate him out of existence),让系统直接查表。
表格抽象
put(key1, key2, value):把 value 存到以 key1、key2 为键的位置。get(key1, key2):取出对应的 value;若没有则返回空表(null)。
理解:表格本身也是抽象数据,其实现细节(如何用表结构实现)是另一个"George 的问题",本节不展开。
George 和 Martha 的责任
每人负责建立自己那一列:
- George:
put(rectangular, real-part, real-part-rectangular),以及 imag-part、magnitude、angle 各一行。 - Martha:
put(polar, real-part, real-part-polar),等等。
Harry 来了怎么办? 他自己建新的一列即可,不需要任何人修改已有代码。
operate 过程
(define (operate op obj)
(let ((proc (get (type obj) op)))
(if (not (null? proc))
(proc (contents obj))
(error "undefined operator"))))
工作流程:
- 用对象的类型和操作名作为两个键去查表。
- 如果查到过程,剥掉类型,把内容传给它。
- 如果没查到,报错 "undefined operator"。
通用选择器的定义
(define (real-part z) (operate 'real-part z))
(define (imag-part z) (operate 'imag-part z))
(define (magnitude z) (operate 'magnitude z))
(define (angle z) (operate 'angle z))
operate + 表格,完全取代了原来的经理。
十、数据导向编程的执行示例 [43:30]
设 z 是 Martha 的复数:模 1、角 2,标签 polar。
求 (real-part z):
real-part展开为(operate 'real-part z)。operate查表,用(type z)=polar和real-part作为键。- 查到 Martha 放进表里的过程
real-part-polar。 - 剥掉 z 的类型,把内容
(1 . 2)交给real-part-polar。 - 得到结果。
核心思想:数据对象自身携带着"如何操作它"的信息。这叫数据导向编程(data-directed programming)。
课堂问答要点
问:数据对象里存了什么?操作存在哪里?
- 数据对象里存的是:数据本身(如
(1 . 2))+ 类型符号(如符号polar)。 - 操作存在表格里。表格的行列标签是符号;存进去的值是过程。
易错点: 讲课时写 magnitude-polar 容易让人以为是符号,实际上它指的是过程 magnitude-polar。更准确的写法是用 lambda。
拓展:命名冲突的另一种解法——George 和 Martha 根本不必给过程起名,直接把 lambda 生成的匿名过程塞进表格即可。
拓展:消息传递(message passing)——另一种系统组织方式:不存类型符号,而是把所有操作本身存进数据对象里。
十一、把复数系统嵌入通用算术系统 [48:00]
前面只是展示了机制,真正的威力要在更复杂的系统中才显现。
设想一个通用算术系统(generic arithmetic system):
- 顶层:
add、sub、mul、div通用操作符 - 中间:抽象屏障
- 下层:复数包、有理数包、普通数包
有理数包的接入
有理数包本身完全不用改(上一讲的代码原样保留),只需:
- 构造有理数时贴上类型标签
rational。 - 把操作装进表格:
put(rational, add, +rat)put(rational, sub, -rat)put(rational, mul, *rat)put(rational, div, /rat)
注意: 表格的键是符号(要 quote),值是过程。
operate-2 过程
(define (operate-2 op arg1 arg2)
(if (eq? (type arg1) (type arg2))
(let ((proc (get (type arg1) op)))
(if (not (null? proc))
(proc (contents arg1) (contents arg2))
(error "undefined operator")))
(error "types differ")))
考点: 与 operate 的关键区别——先检查两个参数类型是否相同,不同则报错。
通用操作符的定义
(define (add x y) (operate-2 'add x y))
(define (sub x y) (operate-2 'sub x y))
(define (mul x y) (operate-2 'mul x y))
(define (div x y) (operate-2 'div x y))
复数包的接入
make-complex:把 George/Martha 给的复数贴上类型complex。- 内部过程
+C、-C、*C、/C保持不变。 - 对外过程
+complex等,区别仅在于返回值带类型。 - 装表:
put(complex, add, +complex)等。
普通数包的接入
make-number:给普通数贴上类型number。+number:用普通加法,结果贴上类型。- 装表:
put(number, add, +number)等。
十二、多层类型链的执行示例 [55:00]
计算 (3+4i) × (2+6i):
数据的嵌套结构(自顶向下):
(complex . (rectangular . (3 . 4)))
执行流程:
mul调用operate-2。operate-2看到类型是complex,查表得到*complex,剥掉complex标签。- 进入复数世界,
*C需要取magnitude。 - 取
magnitude又触发operate,看到类型是rectangular,查表得到magnitude-rectangular,再剥一层标签。 - 交给 George 的过程处理。
类型链(chains of types):链的长度等于你在表格中下降的层数。每当有一个垂直屏障、存在"该往哪层走"的歧义时,类型就是在告诉你该往哪走。底层构造数据时,再把自己的类型贴回去。
十三、多项式与递归类型塔 [59:00]
多项式的表示
多项式也是类型化数据对象:
- 类型标签:
polynomial - 变量:如
x - 项表(term-list):一串
(order . coefficient)对
例:x¹⁵ + 2x⁷ + 5 表示为项表 ((15 . 1) (7 . 2) (0 . 5))。
思考:多项式表示方式有很多种,各有取舍(tradeoff),这是符号代数系统的经典设计问题。
多项式加法
(define (add-poly p1 p2)
(if (same-variable? p1 p2)
(make-poly (variable p1)
(add-terms (term-list p1) (term-list p2)))
(error "polys not in same var")))
装表:put(polynomial, add, +poly)。
add-terms 的递归结构
对两个项表 L1、L2 做递归归并:
- 若 L1 为空,返回 L2;若 L2 为空,返回 L1。
- 取各自首项 T1、T2,比较阶数:
- order(T1) > order(T2):递归合并 L1 的剩余项与 L2,再把 T1 拼到最前。
- order(T1) < order(T2):递归合并 L1 与 L2 的剩余项,再把 T2 拼到最前。
- order(T1) = order(T2):递归合并剩余项,再构造一个新项,阶数不变,系数为两系数之和。
十四、递归类型塔的威力 [1:03:00]
关键洞察: add-terms 里那个"系数相加"用的不是普通 +,而是通用 add。
由此自动获得:
- 系数可以是有理数:
(2/3)x² + (5/7)x + 11/4 - 系数可以是复数:
(3+2i)x⁵ + (4+7i) - 系数可以是任意系统能加的东西
更进一步: 因为多项式本身也被装进了表格(put(polynomial, add, +poly)),所以多项式自己也是可以被加的东西。于是可以构造:
- 变量为 y、系数为"x 的多项式"的多项式
- 系数是"系数是有理数的多项式"的多项式
- ……无限递归下去
考点:多项式是一个"类型构造器"(constructor for types)——给它一个类型(如整数),它返回"以该类型为系数的 x 的多项式"类型。对多项式的操作归约为对其系数的操作。
推广:有理对象与矩阵
一般有理对象:(3x+7)/(x²+1),分子分母都是多项式。
只需把有理数包里的 +、* 换成通用操作符 add、mul,整个系统立刻能处理这类对象。
2×2 矩阵:加法归约为元素相加,同样方式接入。于是可以处理"元素是分子分母为多项式的有理对象"的矩阵。
本质:去中心化控制(decentralized control)。 进入
add时,没有人检查"你是否在官方可加名单上",而是说"你自己去按你的部件喜欢的方式相加"。结果是自动涌现出极其复杂的层次结构。
十五、课堂讨论:结构性与复杂性 [1:10:00]
问:这样是不是破坏了上下层的清晰分界?下层定义依赖了上层的东西?
- 答:这只是递归。说"加这些要用那个",并不是结构更差,只是递归结构。
问:改一下 add 会有巨大后果,你甚至不知道影响范围?
- 答:这要看你怎么定义"一致性"。比如本节忽略了 GCD(最大公约数)问题;如果突然决定
+rat要做 GCD 计算,那么它立刻对所有上层可用。好处是大部分情况下这正是你想要的。
问:类型结构是运行时演化出来的,不是定义时就固定的?
- 答:正确。类型结构是递归的、随系统运行演化的,无法用有限清单预先枚举。要描述这样的系统,必须用递归结构。
问:用户不知道 rectangular / polar,怎么构造数据?
- 答:用户手里会有构造器(如
make-complex),或读入循环(reader loop)提供某种输入复数的格式。用户只需说mul,系统自己搞定。
问:改一种表示要改多少代码?
- 答:只改你要改的那一处。比如 Martha 想把对里 angle 和 magnitude 的顺序调换,她只做局部修改,整个系统会自动正确传播。新增一种有理数表示,只要把操作装进表格,所有上层(包括系数是系数是多项式的对象)自动获得该表示。
问:这例子是不是太刻意了?
- 答:部分是。这是从符号代数领域提炼出的方法论,很多人摸爬滚打才总结出来。但确实能说明设计复杂系统的真实困境:有很多种部件、甚至不知道将来要对部件做哪些操作,如何组织操作使得组合起来"自动免费地"工作。
十六、系统的局限:强制类型转换(Coercion)[1:16:00]
本节系统的一个明显破绽:
如果执行 (add 3 (make-rational 7 8))——一个 number 类型、一个 rational 类型——operate-2 会说"类型不同,我不知道怎么加",直接报错。
理想行为: 系统应该能自动把 3 转成 3/1,交给有理数包处理。
这就是 coercion(强制类型转换)问题:既然费尽心思把各种类型建成不同对象,很多时候又需要知道"如何把普通数看成有理数""如何把普通数看成复数"。
真正的复杂性从这里开始:这份知识该放在哪里?
- 是有理数包知道"普通数可能是它的一部分"?
- 还是复数包负责把有理数转成"实部为有理数的复数"?
- 谁负责
(add 5/7 (make-complex ...))这种混合运算?
思考:Abelson 指出,消息传递(message passing)这套东西很大程度上正是被这类问题激发出来的。而符号代数问题极其复杂,处于前沿的人始终处在"摸爬滚打、蒸馏经验"的状态。
十七、再次讨论复杂性 [1:19:00]
问:下层改动很灵活,但上层操作实际上被"冻结"了?改上层不知道影响会扩散到哪里?
答:这是极好的问题。比如要新增一个通用操作 equality-test,那么所有表示包都必须决定"我是否支持相等测试",方法是去查表。
拓展:可以进一步去中心化——不只在数据里存类型符号,而是把操作本身存进每个对象(即消息传递)。
拓展:碎片化系统——像 greatest-common-divisor 这种只对整数有意义、对一般数无意义的过程,会让系统非常碎片化。你可以根据想要的灵活性,在整个谱系上选择把灵活性建在哪里。
结论: 数据导向编程的弱点在于——顶层必须就"有哪些通用操作"达成某种一致,或者至少人们得考虑过它们。你也可以选择一张非常稀疏的表,只放少数几项。玩法很多。
关键标签 / 关键词
SICP 通用操作符 Generic Operator 抽象屏障 垂直屏障 类型化数据 Typed Data 类型分派 Dispatch on Type 数据导向编程 Data-Directed Programming operate put/get 表格 复数算术 极坐标 直角坐标 有理数 多项式 递归类型塔 去中心化控制 消息传递 Message Passing 强制类型转换 Coercion 符号代数 MIT 6.001 Hal Abelson
生成时间:2026-10-11 14:09:34 生成平台:VideoNote www.xygy.top