附录C:自检问题
本附录不是“考试题集”,而是配合正文使用的阶段性自检清单。它的目标不是考你是否记住了每个公式,而是帮助你判断:
- 哪些概念你已经真正理解了;
- 哪些地方只是“看着眼熟”,但还不能独立推导;
- 下一步应该回看哪一章、补哪一类练习。
如何使用这份自检题?
建议把题目按三种层次来使用:
一、基础掌握
这类问题主要检查你是否掌握了每章的核心定义、规则和最基本例子。
如果这些题做不出来,说明你还不适合直接进入后续章节。
二、推导与分析
这类问题要求你手动做自由变量计算、替换、类型推导、约束生成、合一等操作。
如果这些题做不出来,通常表示你已经“知道这个概念是什么”,但还没有真正会用。
三、综合理解
这类问题要求你把一章内部的多个概念联系起来,或者把不同章节之间的主线串起来。
如果这些题做不出来,不一定说明你前面没学会,更可能说明你还没把本教程的整体结构建立起来。
如何把本附录和其他附录一起用?
本附录最适合和另外两个附录一起配合:
- 附录A:术语表
- 当你卡在“概念分不清”时先查这里;
- 例如:值 / 范式 / 卡住、类型检查 / 类型推断、协变 / 逆变。
- 附录B:符号速查表
- 当你卡在“符号看不顺”时先查这里;
- 例如:
Γ ⊢ t : T、→、∀、<:、⊸。
- 本附录C:自检问题
- 用来判断你是否已经能真正自己做推导、计算和解释。
也就是说,最常见的排错顺序通常是:
- 先看正文对应章节;
- 再查附录A确认术语;
- 再查附录B确认符号;
- 最后回到本附录重新做题。
如果你做题时总在术语或记号上卡住,优先回查附录A、附录B,而不是立刻回正文大段重读。
很多时候,问题并不是“整章没看懂”,而只是某个术语边界或某个记号读法还没稳定下来。
建议的使用节奏
- 每学完一章后,先做该章的“基础掌握”题。
- 如果能顺利完成,再做“推导与分析”题。
- 每学完两到三章后,回头做一次前面章节的“综合理解”题,检查自己是否还能把主线串起来。
- 如果你是第二遍阅读本教程,建议直接优先做“推导与分析”和“综合理解”部分。
建议的做题方式
为了让这份自检题真正起作用,建议尽量避免“看一眼题目,觉得自己会了”这种浏览式做法。更有效的方式通常是:
- 先闭卷回答
- 不看正文、不看附录,先尝试自己写出定义、规则或推导。
- 再对照正文与附录
- 检查自己错在:
- 术语没分清;
- 记号没读准;
- 规则会背但不会用;
- 还是跨章节主线没串起来。
- 检查自己错在:
- 最后做一次最小复现
- 例如:
- 再手算一次替换;
- 再手画一次推导树;
- 再手做一次合一;
- 再手判断一次环境分裂。
- 例如:
如果你能做到“闭卷写出一个最小例子”,通常就说明这个知识点已经开始内化了。
第1章 数学基础
若这一章做题不顺,建议先回看第 1 章正文;若你对“关系 / 推导规则 / 结构归纳 / 推导归纳”这些词本身已经有点混淆,再配合附录A对应条目一起看。
基础掌握
- 什么是二元关系?它与集合之间是什么关系?
- 什么是前序、偏序、等价关系?它们分别需要哪些性质?
- 归纳定义和归纳法各解决什么问题?
- 推导规则的一般形式是什么?什么是公理?
推导与分析
-
设 ,,计算:
-
用归纳定义的方式定义“合法括号串”。
-
用推导规则定义自然数上的“小于等于”关系,并尝试推出:
-
用结构归纳法证明:每个算术表达式的大小(节点数)至少为 1。
-
设有推导规则: 尝试手工构造一个推出 的推导树。
综合理解
- 为什么说后续的语法定义、类型规则和类型安全证明,都离不开“归纳定义 + 推导规则 + 归纳法”这三件工具?
第2章 形式文法与 BNF
若这一章做题不顺,建议先回看第 2 章正文;若你对“终结符 / 非终结符 / 具体语法 / 抽象语法 / AST”这些概念已经混在一起,再配合附录A与附录B一起看。
基础掌握
- 形式文法想解决什么问题?为什么自然语言描述语法不够?
- BNF 中的
::=和|分别表示什么? - 什么是终结符?什么是非终结符?
- 具体语法和抽象语法有什么区别?
推导与分析
- 用 BNF 定义一个简单的布尔表达式语言,包含:
truefalsenotandor
并体现:
not优先级高于andand优先级高于or
-
用你定义的文法推导字符串:
-
为表达式 画出对应的抽象语法树(AST)。
-
试着说明:为什么后续章节里写成 这样的生成式时,通常更应理解为抽象语法对象的归纳定义,而不是完整源代码语法?
综合理解
- 为什么说类型系统主要工作在抽象语法层,而不是源代码表面的具体写法上?
第3章 Lambda 演算基础
若这一章做题不顺,建议先回看第 3 章正文;若你对
FV(t)、[x ↦ s]t、\equiv_\alpha这些记号不熟,先查附录B;若你对“自由变量 / 绑定变量 / α-等价 / 替换 / 新鲜变量”这些术语分不清,再查附录A。
基础掌握
- Lambda 演算(Lambda Calculus)的三种基本构造是什么?
- 什么是绑定变量(bound variable)?什么是自由变量(free variable)?
- 什么叫 α-等价(alpha-equivalence)?
- 替换 的直觉含义是什么?
- 为什么替换时必须避免变量捕获(variable capture)?
推导与分析
- 计算下列项的自由变量集合:
- 判断下列各对项是否 α-等价,并说明理由:
- 与
- 与
- 与
- 计算下列替换,并写出关键步骤:
- ,其中 是自由变量
- 试着解释:为什么“新鲜变量”不是“宇宙中从未出现过的变量”,而是“对当前替换步骤足够新、不会引发冲突的变量”?
综合理解
- 为什么说 α-等价和避免捕获的替换,是后面 β-归约与类型系统形式化的基础?
第4章 Lambda 演算中的计算
若这一章做题不顺,建议先回看第 4 章正文;若你对“β-归约 / redex / 范式 / 发散 / 传名调用 / 传值调用 / 小步语义 / 大步语义”这些术语或记号不稳,先配合附录A、附录B一起查。
基础掌握
- 什么是 β-归约(beta-reduction)?
- 什么是可归约表达式(redex)?
- 什么是范式(normal form)?
- 传名调用(call-by-name)和传值调用(call-by-value)的区别是什么?
- 小步语义(small-step semantics)和大步语义(big-step semantics)分别强调什么?
推导与分析
- 逐步归约下列项:
-
解释为什么下面这个归约不能直接做“无脑替换”: 并给出正确结果。
-
对下列项分别先归约外层和先归约内层: 观察它们是否会得到一致结果。
-
比较下列项在传名调用和传值调用下的行为: 说明为什么两种策略的终止性不同。
-
试着说明:为什么 4.1–4.6 节里可以先用开放项建立策略直觉,而 4.8 节开始又要切换到更严格的小步语义规则?
综合理解
- Church–Rosser 定理保证了什么?
- 为什么本章主要解决“程序如何计算”,而“程序为什么不会卡住”要等到下一章才真正形式化?
第5章 类型系统核心概念
若这一章做题不顺,建议先回看第 5 章正文;若你对
Γ ⊢ t : T、进展性、保持性、类型安全、替换引理这些概念不稳,建议同时回查附录A和附录B。
基础掌握
- 类型系统在这一章里试图排除的核心坏状态是什么?
- 类型判断 的含义是什么?
- 本章对象语言为什么更准确地说是“带布尔值与条件表达式的 STLC 扩展”?
- 什么是进展性(progress)?
- 什么是保持性(preservation)?
推导与分析
-
构造推导树,证明:
-
说明为什么下面这个项无法通过类型检查:
-
构造推导树,证明:
-
用自然语言解释替换引理在保持性证明中的作用。
-
试着区分:
- 声明式类型规则在回答什么问题?
- 语法制导的类型检查过程又在回答什么问题?
综合理解
- 为什么“类型安全 = 进展性 + 保持性”?
- 为什么“良类型程序不会出错”不能被理解成“良类型程序一定正确”?
第6章 一阶类型系统:基本类型构造
若这一章做题不顺,建议先回看第 6 章正文;若你对积类型、和类型、记录类型、递归类型、
fold / unfold、Ref(T)这些构造已经混在一起,先查附录A与附录B,再回来做题。
基础掌握
- 什么是一阶类型系统(first-order type system)?这里的“一阶”主要排除了什么能力?
Bool、Nat、Unit的直觉含义分别是什么?- 什么是积类型(product type)?什么是和类型(sum type)?
- 什么是记录类型(record type)?它与积类型是什么关系?
- 什么是递归类型(recursive type)?为什么它对列表、树这类结构是必要的?
- 什么是引用类型
Ref(T)?
推导与分析
-
解释为什么
if t1 then t2 else t3的两个分支必须有相同类型。 -
写出一个
Bool × Nat类型值的例子,并说明如何分别取出它的两个分量。 -
用自然语言解释类型: 中的值可能长什么样。
-
解释为什么列表类型可以写成:
-
说明为什么引入
Ref(T)之后,类型安全证明需要额外跟踪存储的类型信息。 -
试着说明:为什么第 6 章允许显式写出递归类型
\mu X.T,而这并不等于说第 9 章的 HM 合一就会允许任意“无限类型”自动出现?
综合理解
- 为什么说一阶类型系统的核心任务,是让类型系统能够描述常见的数据形态,而不仅仅是函数?
第7章 二阶类型系统:多态与抽象
若这一章做题不顺,建议先回看第 7 章正文;若你对
∀、∃、ΛX.t、t[T]、参数化多态、存在类型、打包 / 拆包这些概念不稳,先配合附录A与附录B一起看。
基础掌握
- 为什么一阶类型系统无法自然表达“对任意类型都成立的恒等函数”?
- 什么是参数化多态(parametric polymorphism)?
System F比普通 STLC 多了哪两类核心构造?- 全称类型 的直觉含义是什么?
- 存在类型(existential type) 的直觉含义是什么?
- 什么是类型算子(type operator)?
推导与分析
-
写出多态恒等函数的 System F 项,并说明它为什么具有类型:
-
解释为什么: 和 可以看作同一个多态项的不同实例化。
-
比较:
并说明它们在直觉上的区别。
-
用自然语言解释为什么存在类型可以表达“隐藏实现、暴露接口”。
-
试着说明:为什么第 7 章对象语言中的
∀X.T与ΛX.t,不应直接和第 9 章 HM 环境中的类型方案∀α.T混为一谈?
综合理解
- 为什么说二阶类型系统让我们不仅能给项分类,还能对类型本身做抽象?
- 为什么参数化多态与存在类型一起构成了类型化抽象的重要基础?
第8章 子类型
若这一章做题不顺,建议先回看第 8 章正文;若你对
S <: T、T-Sub、安全替换、协变 / 逆变 / 不变这些概念仍然摇摆,先查附录A和附录B,再回来重新看函数子类型的方向。
基础掌握
- 子类型(subtyping)关系 的直觉含义是什么?
- 什么是安全替换原则?
- 为什么
T-Sub(subsumption)是子类型系统的关键规则? - 记录宽度子类型化(width subtyping)是什么意思?
- 为什么函数参数逆变(contravariance),而返回值协变(covariance)?
推导与分析
-
解释为什么下面的子类型关系成立:
-
给出一个具体反例,说明如果函数参数按协变处理,会产生什么类型安全问题。
-
设有: 讨论下列哪一个关系应成立,并说明理由:
-
用自然语言解释为什么引用类型通常不能简单协变或逆变。
-
试着说明:为什么本章主要处理的是“可安全提升”的
<:关系,而不是第 9 章里那种“必须相等”的类型等式与合一问题?
综合理解
- 为什么本章的真正主线不是“列举哪些类型有子类型关系”,而是“如何通过
T-Sub把子类型接入类型系统”?
第9章 类型推断
若这一章做题不顺,建议先回看第 9 章正文;如果你已经把“替换”“类型方案”“泛化”“实例化”“合一”“occur check”混在一起,最好同时回查附录A与附录B,并顺手对照第 7 章重新看“显式多态”和“HM 推断”的区别。
基础掌握
- 类型推断和类型检查的区别是什么?
- 什么是 Curry 风格(Curry-style)项?
- 类型推断为什么可以分解成“生成约束(constraint generation)+ 求解约束”?
- 什么是合一(unification)?
- 什么是 occur check?
- 什么是 let 多态(let polymorphism)?
推导与分析
- 推断下列项的最一般类型:
-
对项 手工生成约束,并说明为什么合一失败。
-
对约束组 手工执行合一步骤,写出每一步的替换传播与最终结果。
-
解释为什么下面的表达式在 HM 中可以通过:
let id = fun x -> x in (id true, id 0)
- 解释为什么下面的表达式在标准 HM 中通常不能通过:
fun id -> (id true, id 0)
- 试着说明:为什么第 9 章里对
\lambda f.\lambda x.f x的推断,最先得到的是一个最一般单态类型;而“全称量化后的类型方案”要到顶层或let绑定语境里才最自然?
综合理解
- 为什么函数类型的合一不能简单把两个子问题的结果做“并集”,而必须传播并组合替换?
- 为什么只有
let绑定触发泛化,而普通函数参数通常保持单态? - 为什么 HM 的
let多态不应被理解成“完整 System F 的自动推断版”?
第10章 子结构类型系统
若这一章做题不顺,建议先回看第 10 章正文;若你对“结构规则 / 环境分裂 / 线性函数 / 仿射资源 / 会话类型”这些概念还没有形成稳定图景,最好同时回查附录A与附录B,并顺手回看第 6 章引用类型、第 8 章变化方向的相关部分。
基础掌握
- 什么是结构规则?
- 交换、弱化、收缩分别表达什么?
- 线性、仿射、相关、有序类型系统分别限制了什么?
- 什么是环境分裂?
- 为什么子结构类型系统不仅关心“值是什么”,还关心“值怎样被使用”?
推导与分析
- 解释为什么“允许收缩”会让资源管理变得危险。
- 说明仿射类型和线性类型的区别,并各举一个更适合它们的资源场景。
- 为什么环境分裂是线性函数应用规则中的关键机制?
- 设想一个“先发送请求,再接收响应”的通信协议,说明为什么单纯的线性使用还不够,还需要顺序约束。
- 解释为什么不能简单说“Rust 就是线性类型系统”或“Rust 就是仿射类型系统”。
- 试着比较:
- 第 5 章主要关心“项是否良类型”;
- 第 10 章进一步关心“变量能否被丢弃、复制或乱序使用”。
说明这两类约束的层次差别。
综合理解
- 常规类型系统为什么会把“变量可自由复制与丢弃”当成默认前提?
- 为什么子结构类型系统可以看作一种把“资源使用纪律”纳入类型系统的方式?
跨章节综合题
下面这些问题适合在你完成正文一遍阅读后再做。
主线回顾
- 用自己的话说明本教程的大主线:
- 语法定义了什么?
- 语义定义了什么?
- 类型系统定义了什么?
- 元理论又说明了什么?
- 为什么要先学 Lambda 演算,再学类型系统?
- 为什么类型安全证明离不开第三章的替换和第四章的操作语义?
对比与联系
- 比较下列几组概念:
- α-等价 vs β-归约
- 值 vs 范式
- 类型检查 vs 类型推断
- 参数化多态 vs 子类型
- 线性类型 vs 仿射类型
- 为什么说:
- 第五章关心“程序为什么不会卡住”
- 第九章关心“程序员不写注解时系统怎么恢复类型”
- 第十章则进一步关心“值应该怎样被使用”
- 试着说明下列两组“全称量化”为什么不能简单混为一谈:
- 第 7 章中的
∀X.T - 第 9 章 HM 环境中的类型方案(type scheme)
∀α.T
- 试着说明下列两组“递归 / 无限”为什么也不能简单混为一谈:
- 第 6 章中的显式递归类型
μX.T - 第 9 章 occur check 所阻止的无限类型(infinite type)
更进一步的反思
- 为什么类型系统必须是“保守的近似”,而不能精确分析程序所有语义行为?
- 为什么“表达力更强”往往会和“推断更难、实现更复杂”同时出现?
- 如果让你用一句话分别概括第 3–10 章各章的核心贡献,你会怎么写?
使用这份自检题时的一个提醒
如果你发现自己:
- 能复述定义;
- 但做不出推导;
- 或者能做局部推导,但说不清章节主线;
这都很正常。它通常说明你正处在“从看懂走向会用”的阶段。
这份附录的作用,不是让你因为不会而沮丧,而是帮你定位:
- 是定义没有真正理解;
- 还是规则不会操作;
- 还是跨章节的主线还没有建立起来。
只要你能据此回到对应章节,有针对性地重做例子和练习,这份自检题就达到了它的目的。
如果你想把这份附录用得更有效,可以把每次做题后的结果简单记成三类:
- 会复述,但不会算
- 会算局部,但不会串主线
- 已经能闭卷做最小例子
这样你在第二轮、第三轮回看本教程时,会更容易判断自己下一步该补哪一类能力。