视频封面

视频元信息

  • 课程名称:6.001 计算机程序的构造与解释(Structure and Interpretation of Computer Programs, SICP)
  • 授课教师:Hal Abelson(MIT)
  • 年份:1986年(传奇原版)
  • 本讲主题:Lisp 导论与课程概览

内容摘要(TL;DR)

本讲是 SICP 课程的开篇。Abelson 教授首先批判"计算机科学"这个名称,指出其本质是形式化"如何做"的过程性知识(imperative knowledge),而非关于计算机本身。随后介绍课程三大主题:黑盒抽象、约定接口、元语言抽象,并以 Lisp 语言为例讲解原始元素、组合手段与抽象手段,最后通过牛顿法求平方根引出递归定义。


一、计算机科学是一门"名不副实"的学科 [00:14]

1.1 为什么"计算机科学"是个糟糕的名字

Abelson 开篇即指出:"计算机科学"(computer science)是一个糟糕的名称。

  • 它不是科学(science):可能更像工程(engineering)或艺术(art),甚至与魔法(magic)有很多共同点。
  • 它也不是关于计算机的:就像物理学不是关于粒子加速器的,生物学不是关于显微镜和培养皿的。

理解:这是一个非常重要的认识论问题。学科的名称往往来源于它最初使用的工具,而非它的本质。

1.2 几何学的类比

考点:几何学(geometry)同样是一个"糟糕的名字"。

  • 词源:geo(大地)+ metron(测量)= 测量大地/测量术
  • 历史背景:几千年前,埃及祭司阶层为了在尼罗河每年泛滥后恢复被冲毁的田界,发展了几何学的雏形。
  • 对当时的埃及人来说,几何学真的就是使用测量仪器。

推导:今天我们回望埃及人,会说他们真正做的、重要的事情,是开始形式化关于空间和时间的概念,开创了精确谈论数学真理的方式,最终导向了公理化方法和整个现代数学。

1.3 计算机科学的本质

重点:计算机科学的本质是形式化"过程性直觉"(intuitions about process)。

  • 几何学谈论的是"什么是真的"(declarative knowledge,声明性知识)
  • 计算机科学谈论的是"如何做"(imperative knowledge,过程性知识)

易错点:我们之所以认为计算机科学是关于计算机的,和埃及人认为几何学是关于测量仪器的,原因完全相同——当一个领域刚刚起步、人们还没真正理解它时,很容易把做事的本质与所用的工具混淆。Abelson 甚至说,在某种绝对尺度上,我们对计算机科学本质的了解,可能还不如古埃及人对几何学本质的了解。


二、声明性知识 vs 过程性知识 [03:57]

2.1 平方根的例子

声明性知识(What is true)——数学上对平方根的定义:

x 的平方根是满足 y² = x 且 y > 0 的数 y。

这是一个漂亮的数学陈述,但它只告诉你平方根是什么,完全没有说如何找到它。

过程性知识(How to do)——如何求平方根:

  • 来源:亚历山大城的海伦(Heron of Alexandria)的算法
  • 方法:逐次平均法(successive averaging)
    1. 做一个猜测(make a guess)
    2. 改进这个猜测:将猜测值与 x/猜测值 取平均
    3. 不断改进,直到"足够好"(good enough)

2.2 过程、程序与语言

  • 过程(process):可以想象成一种"魔法精灵",听从计算机的指令去做事。
  • 程序(procedure):指导过程的规则模式,即"控制魔法精灵的咒语"。
  • Lisp:我们用来"念咒"的魔法语言。

思考:Abelson 用"魔法"作比喻并非随意。过程是看不见、摸不着的抽象存在,程序则是描述过程的符号模式——这正是 SICP 反复强调的"过程"与"程序"的区分。

2.3 学习 Lisp 的类比

重点:Lisp 的规则几分钟就能学完,但掌握其含义需要整门课。

  • 类比下棋:几分钟就能讲完国际象棋的规则,但这与"理解规则的推论、成为大师级棋手"完全不同。
  • 本课程真正难的部分:不是规则本身,而是规则的推论(implications)——如何利用这些规则成为大师级程序员。

三、计算机科学的真正问题:控制复杂性 [07:21]

3.1 大系统的挑战

考点:计算机科学的真正问题出现在构建超大型系统时。

  • 程序可能长达数千页,长到没有人能一次性装进脑子里。
  • 之所以可能构建这样的系统,唯一原因是存在控制复杂性的技术。
  • 这些技术正是本课程的核心内容,也是计算机科学的本质。

3.2 与其他工程学科的关键区别

有人会说:大型客机也是极其复杂的系统,航空工程师同样在处理巨大的复杂性。但计算机科学的复杂性与物理工程有本质区别:

维度 物理工程(如电子工程) 软件工程
组件 真实物理部件 理想化抽象组件
关注点 容差、近似、噪声、干扰 无需担心容差
约束 物理定律、时间、噪声 人类心智的局限

易错点:百万级放大器的例子——电子工程师可以轻松搭建一级、二级放大器,但想象搭建百万级放大器是荒谬的,因为前几级组件的噪声会被逐级放大,使整个系统失去意义。

推导:但计算机科学处理的是理想化组件——我们对程序和数据片段了解得和想要的一样多,它们精确如我们所愿。因此,构建大型程序时,"我能构建的"和"我能想象的"之间差别不大。

结论:计算机科学是一种抽象形式的工程——一种忽略现实约束的工程。


四、课程三大主题概览 [10:38]

4.1 第一主题:黑盒抽象(Black Box Abstraction)

这是所有工程都使用的技术。

  • 把某个东西装进"盒子",对外只暴露接口。
  • 例如:把求平方根的方法装进一个盒子,输入 36 输出 6。
  • 关键:George 想计算 √a + √b 时,可以直接把"平方根盒子"当模块用,无需知道盒子内部。
  • 从 George 的角度看,盒子内部实现无关紧要——我写的是 √x 还是 √y 还是 √a,都不重要。

黑盒抽象的第二个理由:有时你想表达"你的方法"是某个更一般概念的特例,并希望语言能表达这种一般性。

4.2 不动点(Fixed Point)——一般化策略的例子

重点:平方根算法是一般"不动点"策略的特例。

  • 不动点定义:函数 F 的不动点是一个值 y,使得 F(y) = y。
  • 一般策略:从猜测开始,反复应用 F,直到结果不再变化。
  • 平方根是不动点:求 √x 可以转化为求函数 y → (y + x/y)/2 的不动点。
    • 推导:如果 y = √x,那么 x/y = √x,两者相等,平均值不变,即 F(y) = y。

关键洞察:我们不仅要表达"如何求平方根"这种具体过程性知识,还要表达"如何求不动点"这种一般过程性知识。

思考:这意味着"盒子"不仅能输入数字输出数字,还能输入函数、输出方法。这就是高阶过程(higher-order procedures)的雏形——输入和输出本身是过程的过程。

4.3 第二主题:约定接口(Conventional Interfaces)

问题:如何表达"线性组合"这样的一般概念?

  • 对数字:2 × (1 + 3) = 8
  • 对向量:x × (a1 + a2)
  • 对多项式:两个多项式相加再乘以系数
  • 对电信号:两个信号相加再放大

易错点:不同数据类型(数字、多项式、电信号)的底层加法操作显然不同。知识应该放在哪里? 如果明天 George 带来一种新对象,如何把它加入系统而不破坏已有的一切?

解决方案:建立约定接口(conventional interfaces)——就像电子工程中标准阻抗的连接器,只要遵循标准接口,就能互相插接。

第二主题包含的内容:

  1. 通用操作(generic operations):如 + 必须能处理所有类型的数据。
  2. 大型结构:如何组织模拟现实复杂系统的大型程序。
    • 面向对象编程(object-oriented programming):把系统看作"小对象的社会",对象间通过发送信息交互。
    • 流(streams):对聚合体进行操作,像信号处理工程师搭建大型电路那样组织系统。

4.4 第三主题:元语言抽象(Meddling with the Abstraction)

考点:控制复杂性的第三种技术——创造新语言。

  • 当被设计的复杂性压垮时,选择一种新的设计语言来控制复杂性。
  • 新语言的目的:突出系统的不同方面,抑制某些细节,强调另一些细节。
  • 这是课程最"魔法"的部分。

本主题的内容:

  1. 构建 Lisp 解释器:用 Lisp 本身表达 Lisp 的解释过程(自指涉,self-referential)。
    • 理解:解释 Lisp 的过程是两个过程——eval(求值)和 apply(应用)——不断相互归约的"巨大循环"。
  2. Y 算子(Y operator):在过程性语言中表达"无穷"的符号。
  3. 逻辑程序设计语言(logic programming):用完全相同的技术构建一种截然不同的语言,不谈有输入输出的过程,只谈事物之间的关系(relations)。
  4. 硬件实现:在最简单的机器上具体实现这些系统——包括用硬件实现的 Lisp 解释器芯片。

五、语言的通用分析框架 [23:08]

重点:这是本课程最重要的东西——分析任何语言的通用框架。

当有人向你介绍一门"很棒的新语言"时,不要问"求逆矩阵要敲多少个字符"(这无关紧要),而应该问三个问题:

  1. 原始元素(primitive elements):语言自带什么?
  2. 组合手段(means of combination):如何把原始元素组合成更大的东西?
  3. 抽象手段(means of abstraction):如何给复杂的东西命名,使其能像原始元素一样被使用?

思考:这个"三问框架"是 SICP 的核心方法论,贯穿整门课程。它把语言学习从"记忆语法"提升到"理解结构"的层次。


六、Lisp 的原始元素 [24:44]

6.1 原始数据

  • 数字:如 3、17.4、5
  • 易错点(哲学层面的严谨):Abelson 强调,3 不是数字三本身,而是代表柏拉图式"数字三"概念的一个符号。

6.2 原始过程

  • 加法:+ 是"柏拉图式加法概念"的一个名称,如同 3 是数字三的名称。

理解:这种"符号 vs 所指"的区分看似吹毛求疵,但它是理解 Lisp 元语言能力的基础——语言可以谈论语言自身。


七、组合手段:组合式(Combinations)[25:35]

7.1 基本形式

(+ 3 17.4 5)  →  25.4

组合式(combination) 的构成:

  • 操作符(operator):如 +
  • 操作数(operands):如 3、17.4、5

7.2 嵌套组合

(+ 3 (* 5 6) 8 2)  →  43

重点:操作数本身可以是组合式——这是复杂性的来源。

7.3 前缀表示法(Prefix Notation)

  • 操作符写在操作数左边
  • 完全括号化(fully parenthesized),使表达完全无歧义

7.4 括号的严格性

易错点:Lisp 的括号与数学中的括号完全不同。

  • 数学中括号表示分组,有时可以省略,多加也无妨。
  • Lisp 中不能省略括号,也不能多加括号——每个括号都精确地表示"这是一个组合式"。
  • 去掉括号,含义就变了。

7.5 树形结构视角

理解:组合式本质上是一棵树。

  • 括号只是把二维的树结构写成线性字符串的方式。
  • 例如 (+ 3 (* 5 6) 8 2) 的树:
    • 根节点:+
    • 子节点:3、(* 5 6) 子树、8、2
    • (* 5 6) 子树:根为 *,子为 5、6

思考:Abelson 提到,如果 Lisp 诞生于今天,有了图形界面,语法可能会直接画成树。括号是历史妥协的产物。


八、Lisp 解释器实操演示 [29:23]

8.1 基本求值

  • 输入 3 → 返回 3
  • 输入 (+ 3 4 8) → 返回 15
  • 输入嵌套组合 → 返回对应值

8.2 括号辅助功能

重点:许多 Lisp 系统内置两种括号辅助功能:

  1. 括号闪烁(flashing parentheses):输入右括号时,高亮匹配的左括号,帮助追踪括号配对。
  2. 自动缩进 / 美化打印(pretty printing):编辑器自动缩进到正确层级,使嵌套结构一目了然。

易错点:手工处理大量括号很容易出错,必须学会使用这些辅助功能。


九、抽象手段:define [34:28]

9.1 命名值

(define a (* 5 5))
  • 现在 (* a a) 返回 625
  • a 可以在更复杂的组合中使用
(define b (+ a (* 5 a)))
  • 定义时,解释器返回被定义的符号(如 a、b)

9.2 命名过程(一般方法)

重点:我们真正想命名的是"一般方法",如"平方"。

(define (square x) (* x x))
  • 这里 x 是一个代词(pronoun),代表"要被平方的东西"。
  • (square 10) → 100

9.3 lambda:define 的底层形式

(define square (lambda (x) (* x x)))

考点:lambda 是"制造过程"的方式。

  • 读法:"定义 square 为:制造一个过程,其参数名为 x,它返回 x 乘以 x 的结果。"
  • 两种写法对 Lisp 解释器而言完全等价。

9.4 语法糖(Syntactic Sugar)

重点概念:语法糖 = 更方便的表面书写形式。

  • (define (square x) ...) 是 (define square (lambda (x) ...)) 的语法糖。
  • 易错点:不要忘记,写 (define (square x) ...) 时,你实际上是在给某个东西命名,而那个东西是一个正在被构造的过程。

9.5 过程作为一等公民

关键:已定义的过程可以像原始元素一样使用。

  • (square 1001)、(square (+ 5 7))
  • (+ (square 3) (square 4)) → 25
  • (square (square (square 1001))) 嵌套使用
  • 直接求值 square → 返回 #<compound procedure square>(过程的打印表示)

9.6 平均值与均方

(define (average x y) (/ (+ x y) 2))
(define (mean-square x y) (average (square x) (square y)))
  • (mean-square 3 4) → (average 9 16) → 6.5
  • 重点:定义 mean-square 的人无需知道 square 是语言内置的还是用户定义的。

核心原则:Lisp 不在"语言内置的东西"和"用户构建的东西"之间做任意区分。用户构建的东西拥有与原始元素完全相同的威力和灵活性。

演示:在系统中查询 + 的值,返回 #<compound procedure +>——连加法操作符本身都是复合过程!但你永远不会知道,也不应该知道,因为它处于我们处理的抽象层次之下。


十、条件表达式:cond 与 if [43:47]

10.1 绝对值函数的数学定义

|x| = -x,  当 x < 0
    = 0,   当 x = 0
    = x,   当 x > 0

10.2 用 cond 实现

(define (abs x)
  (cond ((< x 0) (- x))
        ((= x 0) 0)
        ((> x 0) x)))

结构说明:

  • cond = 条件表达式(conditional),表示情况分析。
  • 每个子句(clause) 有两部分:
    1. 谓词(predicate):返回真或假的表达式,如 (< x 0)
    2. 动作(action):条件为真时执行的操作,如 (- x)

易错点:Lisp 中减号的两种含义:

  • 两个参数:(- a b) → 减法(从 a 减 b)
  • 一个参数:(- x) → 取负(negation)

10.3 用 if 实现

(define (abs x)
  (if (< x 0) (- x) x))

重点:if 是受限的情况分析(只有一真一假两个分支)。

理解:cond 和 if 本质相同,可以互相定义。实现 Lisp 系统的人会选一个,用另一个来实现它,选哪个都无所谓。


十一、递归定义:牛顿法求平方根 [50:39]

11.1 海伦算法的数学原理

重点:逐次平均法为什么有效?

  • 如果猜测值 G 恰好等于 √x,那么 x/G = √x,平均值不变。
  • 如果 G 大于 √x,则 x/G 小于 √x,平均值落在两者之间。
  • 如果 G 小于 √x,则 x/G 大于 √x,平均值同样落在两者之间。
  • 推导:可以证明,如果 G 与 √x 有偏差,G 与 x/G 的平均值会更接近 √x。反复迭代即可逼近。
  • 另一个事实:总是可以用 1 作为初始猜测,且总能收敛到 √x。

11.2 用 Lisp 实现

(define (try guess x)
  (if (good-enough? guess x)
      guess
      (try (improve guess x) x)))

(define (sqrt x)
  (try 1 x))

(define (improve guess x)
  (average guess (/ x guess)))

(define (good-enough? guess x)
  (< (abs (- (square guess) x)) 0.001))

11.3 模块化结构

  • sqrt 定义在 try 之上
  • try 定义在 good-enough? 和 improve 之上
  • good-enough? 定义在 abs 和 square 之上
  • improve 定义在 average 和原始操作符之上
  • try 还定义在自身之上(递归)

11.4 递归定义展开演示

考点:递归定义如何逐步归约。

求 √2:

  1. (try 1 2) → 1 不够好 → (try (improve 1 2) 2)
  2. (improve 1 2) = (average 1 (/ 2 1)) = (average 1 2) = 1.5
  3. (try 1.5 2) → 1.5 不够好 → (try (improve 1.5 2) 2)
  4. (improve 1.5 2) = (average 1.5 1.333) = 1.333
  5. (try 1.333 2) → 继续...
  6. 最终收敛到约 1.414

易错点:高中数学老师可能告诉你"用自身定义自身是不对的",但这是错误的!有时用自身定义自身完全合理,这就是递归定义(recursive definition)。

重点:递归定义是巨大威力的来源,它实际上让你能进行"无穷计算"。

11.5 内部定义与封装

重点:可以通过内部定义把辅助过程(improve、good-enough?、try)封装在 sqrt 内部。

  • 这样外界看不到这些辅助过程,只看到 sqrt 这个黑盒。
  • 这是黑盒抽象的体现:把内部实现细节打包隐藏在定义内部。

十二、课程内容总结与回顾 [57:48]

12.1 已学到的 Lisp 元素

类别 内容
原始过程 +、*、谓词(<、=)等
原始数据 数字
组合手段 组合式(操作符 + 操作数)、cond、if、define
抽象手段 define(命名)、lambda

理解:Abelson 指出,我们用的这些"原始过程"实际上并非真正的原始元素,但这无关紧要——我们将其当作原始元素使用,不去看内部。

12.2 待填补的空白

重点:目前只处理了原始数据,还未涉及:

  • 如何把原始数据组合成复合数据(compound data)
  • 如何对数据进行抽象,使大块数据能像原始元素一样使用

12.3 后续课程预告

接下来的几讲将讨论:

  1. 我们写的过程与机器中发生的过程之间的联系(即求值模型)。
  2. 如何用 Lisp 的威力谈论一般的约定方法,而不仅是单个小计算。

十三、问答环节 [59:39]

Q1:define 有时带括号((define (f x) ...)),有时不带((define a ...)),区别是什么?

答:

  • (define a ...) 是基本形式:define 后面跟符号,再跟该符号所代表的值。
  • (define (square x) ...) 是定义过程的特殊语法技巧(语法糖)。
  • 区别在于是否在定义过程。

Q2:如果 a 用括号定义会怎样?

答:(define (a) (* 5 5)) 定义的是一个名为 a 的过程(无参数),调用它才返回 25。

对比演示:

操作 (define a (* 5 5)) (define (d) (* 5 5))
输入 a / d 返回 25 返回 #<compound procedure d>
输入 (a) 报错:25 不是可应用的操作符 返回 25

思考:这个对比清晰地展示了"值"与"过程"的区别,以及 define 两种形式的本质差异。


关键标签 / 关键词

SICP Lisp 黑盒抽象 约定接口 元语言抽象 声明性知识 过程性知识 组合式 前缀表示法 define lambda 语法糖 cond if 递归定义 牛顿法 海伦算法 不动点 高阶过程 通用操作 面向对象 流 解释器 eval apply Y算子 逻辑程序设计 原始元素 组合手段 抽象手段 控制复杂性 MIT 6.001


生成时间:2026-10-11 13:52:05 生成平台:VideoNote www.xygy.top