重写与化简

rw 的方向与位置控制、simp 的工作原理与调试、simp only 与 simp lemma 的选择。

作者 David更新于 第 12 篇(共 20 篇)

rw:用等式替换

example (a b c : Nat) (h : a = b) : a + c = b + c := by
  rw [h]

rw [h] 把目标里所有的 a 换成 b,然后自动试一次 rfl。

方向

example (a b : Nat) (h : a = b) : b = a := by
  rw [← h]        -- ← 是反向:把 b 换成 a

←(\l)反向使用等式。这是 rw 最常需要调整的地方——写错方向会报「motive is not type correct」或直接找不到可重写的位置。

多次重写

example (a b c : Nat) (h1 : a = b) (h2 : b = c) : a = c := by
  rw [h1, h2]

rw [h1, h2] 依次执行,等价于 rw [h1]; rw [h2]。

指定位置

example (a b : Nat) (h : a = b) (hyp : a + 1 = 5) : b + 1 = 5 := by
  rw [h] at hyp
  exact hyp

at hyp 重写假设而不是目标。at * 重写所有地方。

只重写第一处

example (a b : Nat) (h : a = b) : a + a = b + a := by
  rw [show a = b from h]    -- 仍会全部重写
  -- 要精确控制用 conv 或 nth_rewrite
example (a b : Nat) (h : a = b) : a + a = b + a := by
  nth_rewrite 1 [h]

nth_rewrite n [h] 只重写第 n 处出现。

rw 是语法级替换,不是语义级。 它按项的结构精确匹配——a + b 与 b + a 是不同的项,rw 不会自动用交换律。

另外 rw 会重写所有匹配处,这常常不是你想要的。需要精确控制时用 nth_rewrite 或 conv。

rw 用 Iff

example (p q : Prop) (h : p ↔ q) (hp : p) : q := by
  rw [← h]
  exact hp

↔ 可以像 = 一样重写命题——这是 Iff 最常见的用法。

simp:自动化简

example (n : Nat) : n + 0 = n := by simp

simp 用一组标记为 @[simp] 的引理反复重写,直到无法继续。

simp 的工作方式

1. 收集所有 @[simp] 引理(库里有几万条)
2. 把它们当作【从左到右】的重写规则
3. 反复应用直到不动点
4. 尝试闭合目标(rfl / 平凡真)

关键点:simp 引理是有方向的——@[simp] theorem foo : a = b 表示「把 a 换成 b」,意味着 b 被认为是「更简单的形式」。

simp?:看它做了什么

example (l : List Nat) : (l ++ []).length = l.length := by
  simp?
  -- Try this: simp only [List.append_nil]

simp? 是排查 simp 问题的第一工具。 它输出实际用到的引理,可以直接替换成 simp only [...]。

为什么要替换:simp only 更快、更稳定。simp 会随着库更新而改变行为(新增 simp 引理可能让原来的证明失败或变化),simp only 锁定了具体引理。

库代码里应该优先用 simp only,探索时用 simp。

控制 simp

simp                          -- 用所有 simp 引理
simp only [foo, bar]          -- 只用这两条
simp [foo]                    -- simp 集合 + foo
simp [-foo]                   -- simp 集合但排除 foo
simp at h                     -- 化简假设
simp at *                     -- 化简所有
simp [*]                      -- 把上下文里的假设也当重写规则
simp_all                      -- 反复化简所有目标与假设

simp [*] 很有用——它会用上下文里的等式假设做重写:

example (a b : Nat) (h : a = b) : a + 0 = b := by simp [*]

声明 simp 引理

@[simp]
theorem my_lemma (n : Nat) : n * 1 = n := Nat.mul_one n

加 @[simp] 要谨慎,两个风险:

  1. 循环:a = b 和 b = a 都标了 simp,会无限重写。simp 有深度限制会报错,但很难查。
  2. 方向错误:把「复杂」的一侧当成右边,会让所有目标变复杂。

判据:右边应该比左边更简单/更规范。n + 0 = n 对(右边简单),n = n + 0 错。

rfl vs simp vs decide

三者都能证「显然的」等式,但机制不同:

tactic机制适用
rfl定义展开后相同2 + 2 = 4
simp重写到不动点需要引理的化简
decide实际计算判定可判定的具体命题
norm_num数值规范化(Mathlib)算术表达式
example : 2 + 2 = 4 := by rfl
example : (2 : Nat) + 2 = 4 := by decide
example : (37 : Nat) * 41 = 1517 := by norm_num
example (n : Nat) : n + 0 = n := by simp

rfl 对含变量的表达式常常失败——n + 0 = n 靠 Nat.add 的定义恰好能 rfl,但 0 + n = n 不行(要归纳)。这个不对称经常让初学者困惑,原因在 Nat.add 是对第二个参数递归定义的。

conv:精确定位

example (a b c : Nat) (h : a = b) : a + c = b + c := by
  conv_lhs => rw [h]

conv 进入「导航模式」,可以精确定位到表达式的某一部分再重写:

conv 命令定位
conv_lhs等式左边
conv_rhs等式右边
conv in pat => ...匹配某个模式的地方
lhs / rhs进一步深入
arg n第 n 个参数
ext x进入 lambda / 全称
example (f : Nat → Nat) (a b : Nat) (h : a = b) : f (a + 1) = f (b + 1) := by
  conv_lhs => arg 1; rw [h]

conv 是最后手段——它冗长且脆弱。先试 nth_rewrite、simp only [...]、或重新组织证明。只有在必须精确控制某一处时才用 conv。

unfold 与 delta

def double (n : Nat) : Nat := n * 2

example (n : Nat) : double n = n * 2 := by
  unfold double

unfold f 展开定义。通常 simp [double] 效果一样且更灵活——把定义当 simp 引理用。

对 def 定义的函数,Lean 会自动生成 double.eq_def 等定理供重写用。

常见问题

motive is not type correct

motive is not type correct

依值类型场景下重写失败——被重写的项出现在其他项的类型里,替换会破坏类型。

解法:换个角度重写(先 generalize),或改用 subst、congr、simp only 试试。这是依值类型的代价之一。

simp made no progress

simp 找不到任何可应用的引理。检查:目标形式是不是你以为的(看 Infoview);需要的引理有没有 import;是不是该先 intro/obtain 展开。

maximum recursion depth

simp 陷入循环。用 simp? 或 set_option maxRecDepth 定位,通常是某条 simp 引理方向有问题。

rw 之后目标没变

rw 只做语法匹配。目标里的 a 可能实际是 id a 或某个定义展开后的形式。用 show 先把目标改写成期望的形式,或用 simp only [h] 代替(simp 会做更多定义展开)。

实践建议

-- 探索阶段:用强力自动化
example (l : List Nat) : (l ++ []).length = l.length := by simp

-- 定稿阶段:换成显式版本,用 simp? 得到
example (l : List Nat) : (l ++ []).length = l.length := by
  simp only [List.append_nil]

先用 simp 探路,再用 simp? 固化。 这个流程既保持了开发效率,又让最终证明稳定可维护。

下一篇:归纳证明。

本页目录