形式验证是使用数学方法严格证明程序或系统正确性的技术。不同于测试(只能发现存在的错误),形式验证可以证明错误的不存在。
形式验证的目标是,将一段程序的功能用另一段程序表达,并且再用另一段程序来表示该程序符合给定的功能要求。
Coq安装h2
Coq 提供了Linux、Windows和MacOS 在内的多系统安装。但是由于博主电脑问题或者其他一些原因,并没有成功安装其Windows环境,只能想办法在WSL2中,即Linux中使用COQ。 也幸亏之前有过WSL2+VsCode经验,不然又少不了一番折腾。
安装opamh3
Linux 和MacOs 可以使用 opam 官网提供的脚本一键安装。
bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh)"
注意: 脚本会将 opam 默认安装到 /usr/local/bin
,如不修改请确保当前用户该路径有读写权限(sudo)
。
安装完成后可以使用下面命令确认是否安装成功。如果成功将返回 opam的版本号,为确保Coq 安装成功,请安装最新版 opam。
opam --version
安装成功后需要执行下面命令 初始化 opam 状态:
opam init
至此 opam 安装配置完成。
使用 opam 安装 Coqh3
首先确保 opam 安装成功并初始化,接下来可以使用下面命令自动安装,由于 Coq 需要从源码编译所以安装时间较长:
opam pin add coq 8.20.0
注意:Coq 最新版本可能已更新,如果想查看最近版本请访问 或者其 Coq官网 github仓库
以上述命令安装 Coq 不会自动更新,若更新请执行:
opam pin add coq <版本号>
使用下面命令可以查看 Coq 是否被正确安装:
coqc --version
配置 VsCoqh3
首先在 vscode 中安装扩展 VsCoq。接下来按照 的说明,依次运行两个下载命令: 插件说明页
opam pin add coq 8.18.0opam install vscoq-language-server
这会在 ubuntu 上安装 language server,接下来为了让 vscode 找到这个 server,你需要运行
which vscoqtop
注意
如果此时没有输出(即失败),可能是没有刷新。运行
eval $(opam env)
此时你将得到一个类似于
`/home/$USERNAME$/.opam/default/bin/vscoqtop`
的输出,将其完整地复制下来,填入 VsCoq 的 Extension Setting 中,VsCoq: Path
的值。
有人在 Windows 下配环境变量配习惯了,把路径复制到
../bin
就停了。但是此时必须要完整地复制全部内容。
COQ语法介绍h2
在浏览软件基础这本书时,发觉它对入门者的照顾并不是特别友好,因此,关于Coq的语法规则,我使用Cluade大模型来进行一步步的讲解。
1. 基本定义和声明h3
1.1 定义常量h4
使用 Definition 来定义常量或函数。
Definition pi := 3.14.Check pi. (* 输出: pi : R *)
1.2 定义函数h4
以下是一个定义简单加法函数的例子:
Definition add (a b : nat) : nat := a + b.Check add.(* 输出: add : nat -> nat -> nat *)
2. 数据类型h3
2.1 自然数(nat)h4
Coq 内置了自然数类型 nat,并提供了相关操作。
Check 0. (* 输出: 0 : nat *)Check 1. (* 输出: 1 : nat *)
2.2 布尔型(bool)h4
布尔型在 Coq 中也很常用。
Check true. (* 输出: true : bool *)Check false. (* 输出: false : bool *)
3. 归纳类型h4
你可以定义自己的数据结构,例如列表。
Inductive my_list (A : Type) : Type := | nil : my_list A | cons : A -> my_list A -> my_list A.
4. 函数定义与模式匹配h3
4.1 定义函数h4
使用 Fixpoint 定义递归函数。以下是一个计算列表长度的例子:
Fixpoint my_length {A : Type} (l : my_list A) : nat := match l with | nil _ => 0 | cons _ _ l' => 1 + my_length l' end.
5. 证明结构h3
5.1 声明定理h4
使用 Theorem 来声明一个新的定理。
Theorem zero_plus : forall n : nat, 0 + n = n.Proof. (* 证明将在这里进行 *)Qed.
5.2 使用策略h4
以下是使用策略证明加法的性质:
Theorem zero_plus : forall n : nat, 0 + n = n.Proof. intro n. (* 引入变量 n *) simpl. (* 简化表达式 *) reflexivity. (* 证明两边相等 *)Qed.
5.3 归纳法证明h4
归纳法证明的基本结构如下:
Theorem plus_0_r : forall n : nat, n + 0 = n.Proof. intro n. (* 使用归纳法 *) induction n as [| n' IHn']. - simpl. reflexivity. (* 基础情况 *) - simpl. rewrite -> IHn'. reflexivity. (* 归纳步骤 *)Qed.
6. 条件语句和循环h3
Coq 使用模式匹配来定义条件和处理逻辑。
Definition is_zero (n : nat) : bool := match n with | 0 => true | _ => false end.
7. 逻辑运算h3
Coq 提供了基本的逻辑运算,例如与(and)、或(or)、非(not)。
7.1 逻辑与h4
Definition and_example (p q : Prop) : Prop := p /\ q.
7.2 逻辑或h4
Definition or_example (p q : Prop) : Prop := p \/ q.
7.3 逻辑非h4
Definition not_example (p : Prop) : Prop := ~p.
8. 变量和类型h3
8.1 类型声明h4
你可以声明变量和其类型:
Definition x : nat := 5.Check x. (* 输出: x : nat *)
9. 注释h4
Coq 允许使用 (* … *) 进行注释:
(* 这是一个注释 *)
一周七日h3
定义类型h4
10 collapsed lines
(*这段Coq代码定义了一个新的归纳类型(Inductive Type),名为 day。可以这样理解:Inductive day : Type: 这部分声明了我们要创建一个名为 day 的新类型。Inductive 关键字表示这个类型是通过列举其所有可能的构造函数(constructor)来定义的。: Type 表示 day 本身是一个类型。:=: 这是定义操作符,意思是“被定义为”。| monday | tuesday ... | sunday: 这一长串由 | 分隔的列表是 day 类型的构造函数。每一个构造函数都是 day 类型的一个唯一、离散的值。简单来说,这段代码是在创建一个枚举类型,就像在其他编程语言中一样。 它创建了一个名为 day 的新类型,这个类型的变量只能取 monday, tuesday, wednesday, thursday, friday, saturday, sunday 这七个值中的一个。*)
Inductive day : Type :=| monday| tuesday| wednesday| thursday| friday| saturday| sunday.
12 collapsed lines
(*match d with ... end:
这是一个模式匹配表达式,是Coq(以及许多函数式编程语言)中非常核心和强大的功能。它的作用是检查变量 d 的具体值,并根据不同的值执行不同的代码分支。你可以把它看作是其他语言中 switch-case 语句的更强大版本。| monday => tuesday:
这是一个匹配分支。| 分隔了不同的情况。它表示:“如果 d 的值是 monday,那么 => 整个表达式的结果就是 tuesday”。*)Definition next_weekday (d : day) : day :=match d with| monday => tuesday| tuesday => wednesday| wednesday => thursday| thursday => friday| friday => monday| saturday => monday| sunday => mondayend.
(* 测试:计算 next_weekday friday 的结果(应返回 monday) *)Compute (next_weekday friday). (* 输出:monday : day *)
(* 定义一个测试用例命题: "连续两次对 saturday 调用 next_weekday 的结果应等于 tuesday" 解释: 1. 第一次 next_weekday saturday → monday 2. 第二次 next_weekday monday → tuesday*)Example test_next_weekday: (next_weekday (next_weekday saturday)) = tuesday.
(* 证明过程: - simpl:化简表达式(计算具体值) - reflexivity:检查等式两边是否相同 - Qed:完成证明*)Proof. simpl. (* 展开计算:next_weekday saturday → monday 然后 next_weekday monday → tuesday *) reflexivity. (* 验证 tuesday = tuesday *)Qed.
(* 检查 test_next_weekday 的类型,确认它是一个已被证明的命题 *)Check test_next_weekday. (* 输出:test_next_weekday : ... = tuesday *)
如上所述,有了上面这个例子,之后的一些内容就可以看懂了,此处不再赘述。
练习(✨) standard (nandb)h4
Definition nandb (b1:bool) (b2:bool) : bool :=(*一开始不知道match可以匹配两个参数*) match b1, b2 with | true, true => false | _, _ => true end.
Example test_nandb1: (nandb true false) = true.Proof. reflexivity. Qed.
Example test_nandb2: (nandb false false) = true.Proof. reflexivity. Qed.
Example test_nandb3: (nandb false true) = true.Proof. reflexivity. Qed.
Example test_nandb4: (nandb true true) = false.Proof. reflexivity. Qed.
# Coq 中的 `simpl` 和 `reflexivity` 总结
## 1. 为什么例子不加 `simpl` 也能通过?- `reflexivity` 会自动进行隐式计算- 能直接处理像 `nandb` 这样的透明定义- 自动验证化简后的等式(如 `true = true`)
## 2. `simpl` 的作用| 功能 | 说明 ||------|------|| 显式化简 | 展开函数定义、计算 match 分支 || 调试用途 | 观察中间计算步骤 || 复杂表达式 | 需要分步计算时使用 |
## 3. 策略对比| 策略 | 功能 | 自动计算 ||------|------|---------|| `reflexivity` | 检查等式一致性 | 是 || `simpl` | 仅进行表达式化简 | 否 |
## 4. 必须使用 `simpl` 的情况```coqExample: (1 + 1) + (1 + 1) = 4.Proof. simpl. (* 必须显式化简 *) reflexivity.Qed.
练习(✨) standard (andb3)h4
Definition andb3 (b1:bool) (b2:bool) (b3:bool) : bool :=match b1,b2,b3 with| true,true,true => true| _,_,_ => falseend.
Example test_andb31: (andb3 true true true) = true.Proof. simpl. reflexivity. Qed.Example test_andb32: (andb3 false true true) = false.Proof. simpl. reflexivity. Qed.Example test_andb33: (andb3 true false true) = false.Proof. simpl. reflexivity. Qed.Example test_andb34: (andb3 true true false) = false.Proof. simpl. reflexivity. Qed.
练习(✨) standard (factorial)h4
Require Import Coq.Arith.Arith.
Fixpoint plus (n m : nat) : nat := match n with | O => m | S n' => S (plus n' m) end.
Module NatPlayground2.Fixpoint mult (n m : nat) : nat := match n with | O => O | S n' => plus m (mult n' m) (* 正确的递归乘法 *) end.Example test_mult1: (mult 3 3) = 9.Proof. simpl. reflexivity. Qed.
Fixpoint factorial ( n : nat) : nat :=match n with | O => 1 | S n' => mult n (factorial n') end.Example test_factorial1: (factorial 3) = 6.Proof. simpl. reflexivity. Qed.Example test_factorial2: (factorial 5) = (mult 10 12).Proof. simpl. reflexivity. Qed.
练习(✨) standard (ltb)h4
(* 导入标准库(确保 eqb 和 leb 已定义) *)Require Import Coq.Arith.Arith.
(* 方案1:直接模式匹配 leb 和 eqb 的结果 *)Definition ltb_v1 (n m : nat) : bool := match (n <=? m), (n =? m) with | true, false => true (* n ≤ m 且 n ≠ m 时返回 true *) | _, _ => false (* 其他情况返回 false *) end.
(* 方案2:嵌套 if 表达式 *)Definition ltb_v2 (n m : nat) : bool := if (n <=? m) then (* 先检查 n ≤ m *) if (n =? m) then (* 再检查是否不等于 *) false (* n = m 时返回 false *) else true (* n < m 时返回 true *) else false. (* n > m 时返回 false *)
(* 方案3:独立递归实现(不依赖 leb/eqb) *)Fixpoint ltb_v3 (n m : nat) : bool := match n, m with | O, S _ => true (* 0 < 任何正数 *) | S n', S m' => ltb_v3 n' m' (* 递归比较前驱 *) | _, _ => false (* 其他情况(包括 n=O,m=O 和 n>S _,m=O) *) end.
(* 方案4:利用 leb 的否定(最简洁数学定义) *)Definition ltb_v4 (n m : nat) : bool := negb (m <=? n). (* n < m 当且仅当 ¬(m ≤ n) *)
(* 为所有方案声明统一的中缀记法 *)Notation "x <? y" := (ltb_v1 x y) (at level 70). (* 可替换为 v2/v3/v4 *)
(* ===================== 测试用例 ===================== *)Section Tests.(* 测试用例验证所有方案等价 *)Example test_all1: ltb_v1 2 2 = false /\ ltb_v2 2 2 = false /\ ltb_v3 2 2 = false /\ ltb_v4 2 2 = false.Proof. simpl; auto. Qed.
Example test_all2: ltb_v1 2 4 = true /\ ltb_v2 2 4 = true /\ ltb_v3 2 4 = true /\ ltb_v4 2 4 = true.Proof. simpl; auto. Qed.
Example test_all3: ltb_v1 4 2 = false /\ ltb_v2 4 2 = false /\ ltb_v3 4 2 = false /\ ltb_v4 4 2 = false.Proof. simpl; auto. Qed.
(* 边界测试:0 的比较 *)Example test_zero: ltb_v1 0 0 = false /\ ltb_v3 0 1 = true.Proof. simpl; auto. Qed.End Tests.
(* 方案选择建议 *)(*1. 需要清晰逻辑:选择 v1 或 v22. 需要独立实现:选择 v33. 需要数学简洁性:选择 v44. 实际开发推荐直接使用标准库的 Nat.ltb*)
指令 | 用途 | 正式性 | 是否可后续引用 |
---|---|---|---|
Example | 测试用例 | 非正式 | 否 |
Lemma | 辅助引理 | 正式 | 是 |
Theorem | 重要结论 | 最正式 | 是 |
Coq 策略 reflexivity
与 simpl
对比文档h3
核心差异对比h4
策略 | 化简行为 | 设计目的 |
---|---|---|
reflexivity | 自动展开所有定义,直到等式两边完全一致或无法化简 | 快速验证等式成立(不关心中间步骤) |
simpl | 仅进行单步化简,保留用户可读的中间形式 | 调试和观察化简过程 |
示例印证h4
Definition double n := n + n.
Lemma demo_diff : double 2 = 4.Proof. (* 使用 simpl 时: *) simpl. (* 目标变为 2 + 2 = 4,保留可读形式 *)
(* 使用 reflexivity 时: *) reflexivity. (* 直接展开所有定义完成证明,不显示中间状态 *)Qed.
使用建议
-
优先使用 reflexivity: 当等式显然成立时(如算术运算) 希望自动处理所有展开时
-
优先使用 simpl: 需要观察中间化简状态时 避免过度展开导致表达式混乱
练习(✨) standard (plus_id_exercise)h4
Theorem plus_id_exercise : forall n m o : nat, n = m → m = o → n + m = m + o.Proof. (* 引入所有前提 *) intros n m o H1 H2. (* H1 : n = m H2 : m = o 目标:n + m = m + o *)
(* 先用 H1 将目标中的 n 替换为 m *) rewrite -> H1. (* 新目标:m + m = m + o *)
(* 再用 H2 将目标中的 m 替换为 o *) rewrite -> H2. (* 新目标:o + o = o + o *)
(* 两边已完全相同 *) reflexivity.Qed.
但是,此时存在疑问🤔?为什么假定H1就代表了n=m呢?这是COQ的规定吗?或者说我漏掉了什么东西?
经查阅,有以下结论:
在 Coq 中,H1 代表 n = m 是由 intros 策略的语法规则和逻辑结构决定的,这是 Coq 的基本设计约定,但并非随意规定,而是基于逻辑推理的自然形式化。
-
intros 策略的工作机制 当证明一个形如 ∀ n m, n = m → m = o → … 的定理时: intros n m o H1 H2 的执行过程: 按顺序引入 ∀ 量词变量:n, m, o 按顺序引入 → 的前提: 第一个前提 n = m 被命名为 H1 第二个前提 m = o 被命名为 H2 自动命名规则: 若用户不显式指定名称(如 intros n m o Hfoo Hbar),Coq 会按 H, H0, H1… 自动生成名称 名称本身可自定义,但位置顺序必须对应前提的顺序
-
为什么 H1 对应 n = m? 这是由定理的逻辑结构顺序决定的:
Theorem plus_id_exercise : ∀ n m o, n = m → (* 第一个前提 *) m = o → (* 第二个前提 *) n + m = m + o.
intros 从左到右依次处理: 先处理 n = m,命名为 H1, 再处理 m = o,命名为 H2
-
底层逻辑依据 在类型论中,→ 对应函数类型: A → B 可理解为“给定一个 A 类型的值,可得到 B 类型的值” 在证明中,intros H 相当于: 假设你已有一个 A 的证明(即前提 H : A) 目标是构造 B 的证明
-
用户自定义示例 你可以自由重命名前提,但顺序不变:
Theorem plus_id_exercise : ∀ n m o, n = m → m = o → n + m = m + o.Proof. intros x y z Eq_nm Eq_mo. (* 自定义名称 *) rewrite -> Eq_nm, -> Eq_mo. (* 使用新名称 *) reflexivity.Qed.
Eq_nm 仍对应第一个前提 n = m(即 x = y) Eq_mo 仍对应第二个前提 m = o(即 y = z)
练习(✨✨) standard (andb_true_elim2)h4
Theorem andb_true_elim2 : forall b c : bool,andb b c = true -> c = true.Proof. intros b c H. (* 引入假设 H: andb b c = true *) destruct b eqn:Eb. (* 分情况讨论b *) - (* b = true *) simpl in H. (* H变为 c = true *) apply H. (* 直接使用假设 *) - (* b = false *) simpl in H. (* H变为 false = true *) discriminate H. (* 排除不可能情况 *)Qed.
当然,使用rewrite
也是可以的,以下是rewrite版本:
Theorem andb_true_elim2 : ∀ b c, andb b c = true → c = true.Proof. intros b c H. destruct b eqn:Eb. - (* b = true *) simpl in H. (* H : c = true *) rewrite H. (* 目标从 c=true 变为 true=true *) reflexivity. (* 验证 *) - (* b = false *) simpl in H. (* H : false = true *) discriminate H.Qed.
特性 | apply | rewrite |
---|---|---|
核心作用 | 将假设/定理的结论匹配当前目标 | 按等式替换表达式中的部分 |
底层逻辑 | 反向推理(从结论反推前提) | 符号重写(表达式转换) |
典型使用场景 | 假设与目标完全一致时 | 需要修改目标或假设中的子表达式时 |
是否需要额外步骤 | 直接完成证明 | 通常需要配合 reflexivity |
语法示例 | apply H. (H: A → B,目标为B) | rewrite H. (H: x = y) |
执行结果 | 用假设直接闭合目标 | 替换后可能仍需验证 |
类比数学证明 | ”根据定理T,结论成立" | "将式子中的x替换为y” |
终极选择指南
能用 apply
时优先用 —— 当假设与目标形状完全匹配
必须用 rewrite
时 —— 当需要表达式转换或部分替换
复杂场景组合用 —— 例如先 rewrite
修改目标,再 apply
匹配定理