张贴作者:Jesper2019年10月4日
我经常听到人们说他们对学习感兴趣阿格达,太棒了!然而,我经常觉得还不够关于如何使用Agda的不同功能的示例实现程序,在其类型中强制执行不变量,并证明其属性。因此,在这篇博客文章中,我希望能准确地解决这个问题问题。
这个例子的主要目的是展示Agda的双重目的作为强类型编程语言,并作为数学形式化。因为这两个目的是相同的语言,它们彼此强化:我们可以证明属性我们可以编写生成证明的程序。在这个例子中,我们将通过(1)定义偏序的数学结构,(2)实现泛型二叉搜索树并在其中插入新元素,以及(3)证明此函数已正确实现。
这篇博客文章是基于我在初始类型俱乐部在查尔默斯。
前期工作
对于这篇文章,我们将依赖性保持在最低限度,因此我们不要依赖标准库。相反,我们导入一些Agda的内置模块。
打开 进口 阿格达。基本体
打开 进口 阿格达。内置。布尔
打开 进口 阿格达。内置。国家
打开 进口 阿格达。内置。平等
A类变量
宣言(由于Agda 2.6.0)允许我们在不绑定的情况下使用变量它们是明确的。这意味着它们被隐含地普遍量化在它们出现的类型中。
变量
A类 B类 C类 : 设置
x个 年 z(z) : A类
k个 我 米 n个 : 国家
在下面的代码中,我们将使用实例论据自动构造一些证明。使用实例时参数它
下面的函数通常非常有用。它所做的一切请Agda使用标记为实例
.(有关实例的更多信息稍后的参数)。
它 : {{x个 : A类}} → A类
它 {{x个}} = x个
(一元)自然数定义为数据类型国家
有两个施工人员零:自然
和例如:Nat→Nat
.我们使用这些从导入阿格达。内置。国家
因为他们允许我们写作文字数字和构造器形式。
_ : 国家
_ = 零 + 7 * (苏克 三 - 1)
(命名的定义_
由Agda排版,但不能稍后使用。这通常用于定义示例或测试用例)。
我们可以定义参数化数据类型和函数模式匹配在他们身上。例如,这里是哈斯克尔的等价物也许 吧
类型。
数据 也许 吧 (A类 : 设置) : 设置 哪里
只是 : A类 → 也许 吧 A类
没有什么 : 也许 吧 A类
地图可能 : (A类 → B类) → (也许 吧 A类 → 也许 吧 B类)
地图可能 (f) (只是 x个) = 只是 ((f) x个)
地图可能 (f) 没有什么 = 没有什么
注意如何A类
和B类
隐式量化为地图可能
!
快速回顾Curry-Howard信件
Curry-Howard信件是我们使用Agda既是一种编程语言,也是一种证明助手。在柯里-霍华德对应关系,我们可以解释逻辑命题(A∧B,A,A⇒B,…)作为所有可能的证明类型。
“A和B”的证明是一对证明的(x,y)x:A
和一个证明年:B
.
记录 _×_ (A类 B类 : 设置) : 设置 哪里
建造师 _,_
领域
有限状态试验 : A类
信噪比 : B类
打开 _×_
“A或B”的证明是英寸x
作为证据x:A
或年
作为证据年:B
.
数据 _⊎_ (A类 B类 : 设置) : 设置 哪里
英制 : A类 → A类 ⊎ B类
内部 : B类 → A类 ⊎ B类
地图输入 : (A类 → B类) → A类 ⊎ C类 → B类 ⊎ C类
地图输入 (f) (英制 x个) = 英制 ((f) x个)
地图输入 (f) (内部 年) = 内部 年
地图输入 : (B类 → C类) → A类 ⊎ B类 → A类 ⊎ C类
地图输入 (f) (英制 x个) = 英制 x个
地图输入 (f) (内部收益率 年) = 内部 ((f) 年)
“A意味着B”的证明是证明的转换x:A
到证明B类
,即类型为的函数A→B
.
“true”正好有一个证明tt:⊤
。我们可以将其定义为具有单个构造函数的数据类型tt公司
,但这里我们将其定义为而是记录类型。这具有Agda将使用的优势元素的eta-e质量⊤
,即x=y
对于任意两个变量x个
和年
类型为⊤
.
记录 ⊤ : 设置 哪里
建造师 tt公司
“false”没有证据。
数据 ⊥ : 设置 哪里
“not A”可以定义为“A implies false”。
¬_ : 设置 → 设置
¬ A类 = A类 → ⊥
示例
除₁ : A类 → (B类 → A类)
除₁ = λ z(z) _ → z(z)
例如 : (A类 × ⊤) → (A类 ⊎ ⊥)
例如 = λ z(z) → 英制 (fst公司 z(z))
不包括(三) : (A类 → B类) → (B类 → C类) → (A类 → C类)
不包括(三) = λ (f) 克 z(z) → 克 ((f) z(z))
不含₄ : ¬ (¬ (A类 ⊎ (¬ A类)))
不含₄ = λ (f) → (f) (内部 (λ x个 → (f) (英制 x个)))
由于阿格达的逻辑是建设性的,因此不可能证明的直接版本出口
(A⊎(\A)
).
平等
要声明程序的许多属性,我们需要以下概念平等。在Agda中,相等被定义为数据类型_≡_
有一个建造师ref:x≡x
(从导入阿格达。内置。平等
).
_ : x个 ≡ x个
_ = 回流
sym(对称) : x个 ≡ 年 → 年 ≡ x个
sym(对称) 回流 = 回流
反式 : x个 ≡ 年 → 年 ≡ z(z) → x个 ≡ z(z)
反式 回流 回流 = 回流
刚果 : ((f) : A类 → B类) → x个 ≡ 年 → (f) x个 ≡ (f) 年
刚果 (f) 回流 = 回流
子集 : (P(P) : A类 → 设置) → x个 ≡ 年 → P(P) x个 → P(P) 年
子集 P(P) 回流 第页 = 第页
排序自然数
自然数的标准排序可以定义为索引数据类型具有两种类型的索引国家
:
模块 自然-≤ 哪里
数据 _≤_ : 国家 → 国家 → 设置 哪里
≤-零 : 零 ≤ n个
≤-suc : 米 ≤ n个 → 苏克 米 ≤ 苏克 n个
≤-回流 : n个 ≤ n个
≤-回流 {n个 = 零} = ≤-零
≤-回流 {n个 = 苏克 k个} = ≤-suc ≤-回流
≤-反式 : k个 ≤ 我 → 我 ≤ 米 → k个 ≤ 米
≤-反式 ≤-零 l≤m = ≤-零
≤-反式 (≤-suc k≤l) (≤-suc l≤m) =
≤-suc (≤-反式 k≤l l≤m)
≤-反对称 : 米 ≤ n个 → n个 ≤ 米 → 米 ≡ n个
≤-反对称 ≤-零 ≤-零 = 回流
≤-反对称 (≤-suc m≤n) (≤-suc n≤m) =
刚果 苏克 (≤-反对称 m≤n n≤m)
现在我们可以证明3 ≤ 5
如下:
_ : 三 ≤ 5
_ = ≤-suc (≤-suc (≤-suc ≤-零))
然而,要证明这样的不等式9000 ≤ 9001
我们必须这样做写9000≤-suc
构造函数,这将变得非常乏味的。相反,我们可以使用Agda的实例参数自动构造此类证明。
为此,我们定义了一个“实例”,它自动构造一个证明m≤n
当m和n是自然数时。A类定义安装:A
标记为“instance”的将用于自动构造具有类型的函数的隐式参数表单的{{x:A}}→B
.
出于效率的原因,我们不标记构造函数≤-零
和≤-suc
直接作为实例。相反,我们利用高效布尔比较_<_
(从导入阿格达。内置。国家
)至当前提条件为So(m<sucn)
是满意的。
所以 : 布尔 → 设置
所以 假 = ⊥
所以 真的 = ⊤
实例
≤-dec : {第页 : 所以 (米 < 苏克 n个)} → 米 ≤ n个
≤-dec {米 = 零} {n个 = n个} = ≤-零
≤-dec {米 = 苏克 米} {n个 = 苏克 n个} {第页 = 第页} =
≤-suc (≤-dec {第页 = 第页})
_ : 9000 ≤ 9001
_ = 它
部分订单
我们想讨论的不仅仅是具体类型的订单,比如国家
,但也涉及“偏序”的一般概念。对于为此,我们定义了一个类型类订单
包含类型_≤_
及其性质的证明。
记录 订单 (A类 : 设置) : 设置₁ 哪里
领域
_≤_ : A类 → A类 → 设置
≤-回流 : x个 ≤ x个
≤-反式 : x个 ≤ 年 → 年 ≤ z(z) → x个 ≤ z(z)
≤-反对称 : x个 ≤ 年 → 年 ≤ x个 → x个 ≡ 年
_≥_ : A类 → A类 → 设置
x个 ≥ 年 = 年 ≤ x个
与Haskell不同,类型类不是阿格达。相反,我们使用特殊语法打开命令{{…}}
带来作用域中记录的字段作为实例使用类型表格{A:Set}{r:OrdA}}→。。。
。然后将执行实例搜索开始寻找typeclass的正确实现自动。
打开 订单 {{...}}
我们现在定义一些具体的类型类的实例使用共模式匹配
实例
订单编号 : 订单 国家
_≤_ {{订单编号}} = 自然-≤._≤_
≤-回流 {{订单编号}} = 自然-≤.≤-回流
≤-反式 {{订单编号}} = 自然-≤.≤-反式
≤-反对称 {{订单编号}} = Nat-≤.≤-反同义词
实例
订单-⊤ : 订单 ⊤
_≤_ {{订单-⊤}} = λ _ _ → ⊤
≤-回流 {{订单-⊤}} = tt公司
≤-反式 {{订单-⊤}} = λ _ _ → tt公司
≤-反对称 {{命令-⊤}} = λ _ _ → 回流
模块 例子 (A类 : 设置) {{A-≤ : 订单 A类}} 哪里
例子 : {x个 年 z(z) : A类} → x个 ≤ 年 → 年 ≤ z(z) → z(z) ≤ x个 → x个 ≡ 年
例子 x≤y y≤z z≤x = ≤-反对称 {A类 = A类} x≤y (≤-反式 {A类 = A类} y≤z z≤x)
对于使用二进制搜索树,我们需要能够决定任何两个较大的元素,即我们需要一个全部的,可判定的订单。
数据 三个 {{_ : 订单 A类}} : A类 → A类 → 设置 哪里
较少的 : {{x≤y : x个 ≤ 年}} → 三个 x个 年
平等的 : {{x≡y : x个 ≡ 年}} → 三个 x个 年
更大的 : {{x≥y : x个 ≥ 年}} → 三个 x个 年
记录 TDO公司 (A类 : 设置) : 设置₁ 哪里
领域
{{订单-A}} : 订单 A类
三个 : (x个 年 : A类) → 三个 x个 年
打开 TDO公司 {{...}} 公众的
triNat公司 : (x个 年 : 国家) → 三个 x个 年
triNat公司 零 零 = 平等的
triNat公司 零 (苏克 年) = 较少的
triNat公司 (苏克 x个) 零 = 更大的
triNat公司 (苏克 x个) (苏克 年) 具有 triNat公司 x个 年
... | 较少的 {{x≤y}} = 较少的 {{x≤y = 自然-≤.≤-苏克 x≤y}}
... | 平等的 {{x≡y}} = 平等的 {{x≡y = 刚果 苏克 x选y}}
... | 更大的 {{x≥y}} = 更大的 {{x≥y = 自然-≤.≤-苏克 x≥y}}
实例
TDO-Nat公司 : TDO公司 国家
订单-A {{TDO-Nat公司}} = 订单编号
三个 {{TDO-Nat公司}} = triNat公司
二进制搜索树
在依赖类型的语言中,我们可以对数据的不变量进行编码结构,使用索引数据类型。在本例中,我们将通过对它们包含的元素(请参见如何留住你的邻居订单通过Conor McBride)。
由于下界可以是-∞,上界可以是+∞,因此我们首先提供扩展部分有序集的通用方法用这两个元素。
数据 [_]∞ (A类 : 设置) : 设置 哪里
-∞ : [ A类 ]∞
[_] : A类 → [ A类 ]∞
+∞ : [ A类 ]∞
变量
降低 上面的 : [ A类 ]∞
模块 Ord-[]∞ {A类 : 设置} {{ A-≤ : 订单 A类}} 哪里
数据 _≤∞_ : [ A类 ]∞ → [ A类 ]∞ → 设置 哪里
-∞-≤ : -∞ ≤∞ 年
[]-≤ : x个 ≤ 年 → [ x个 ] ≤∞ [ 年 ]
+∞-≤ : x个 ≤∞ +∞
[]∞refl : x个 ≤∞ x个
[]∞refl { -∞} = -∞-≤
[]∞refl {[ x个 ]} = []-≤ (≤-回流 {A类 = A类})
[]∞refl { +∞} = +∞-≤
[]∞-反式 : x个 ≤∞ 年 → 年 ≤∞ z(z) → x个 ≤∞ z(z)
[]∞-反式 -∞-≤ _ = -∞-≤
[]∞-反式 ([]-≤ x≤y) ([]-≤ y≤z) = []-≤ (≤-反式 {A类 = A类} x≤y y≤z)
[]∞-反式 _ +∞-≤ = +∞-≤
[]∞-反对称 : x个 ≤∞ 年 → 年 ≤∞ x个 → x个 ≡ 年
[]∞-反对称 -∞-≤ -∞-≤ = 回流
[]∞-反对称 ([]-≤ x≤y) ([]-≤ y≤x) = 刚果 [_] (≤-反对称 x≤y y≤x)
[]∞-反对称 +∞-≤ +∞-≤ = 回流
实例
Ord-[]∞ : {{_ : 订单 A类}} → 订单 [ A类 ]∞
_≤_ {{Ord-[]∞}} = _≤∞_
≤-回流 {{奥德-[]∞}} = []∞-反射
≤-反式 {{Ord-[]∞}} = []∞-反式
≤-反对称 {{Ord-[]∞}} = []∞-反对称
打开 Ord-[]∞ 公众的
我们定义了一些实例来自动构造不等式证明。
模块 _ {{_ : 订单 A类}} 哪里
实例
-∞-≤-I : {年 : [ A类 ]∞} → -∞ ≤ 年
-∞-≤-I = -∞-≤
+∞-≤-I : {x个 : [ A类 ]∞} → x个 ≤ +∞
+∞-≤-I = +∞-≤
[]-≤-I : {x个 年 : A类} {{x≤y : x个 ≤ 年}} → [ x个 ] ≤ [ 年 ]
[]-≤-I {{x≤y = x≤y}} = []-≤ x≤y
现在我们(终于)准备好定义二进制搜索树了。
数据 英国标准时间 (A类 : 设置) {{_ : 订单 A类}}
(降低 上面的 : [ A类 ]∞) : 设置 哪里
叶 : {{l≤u : 降低 ≤ 上面的}}
→ 英国标准时间 A类 降低 上面的
节点 : (x个 : A类)
→ 英国标准时间 A类 降低 [ x个 ]
→ 英国标准时间 A类 [ x个 ] 上面的
→ 英国标准时间 A类 降低 上面的
_ : 英国标准时间 国家 -∞ +∞
_ = 节点 42
(节点 6 叶 叶)
(节点 9000 叶 叶)
请注意实例如何通过自动填写边界已满足!更明确地说,树看起来像跟随:
_ : 英国标准时间 国家 -∞ +∞
_ = 节点 42
(节点 6 (叶 {{l≤u = -∞≤6}}) (叶 {{l≤u = 6≤42}}))
(节点 9000 (叶 {{l≤u = 42≤9000}}) (叶 {{l≤u = 9000≤+∞}}))
哪里
-∞≤6 : -∞ ≤ [ 6 ]
-∞≤6 = 它
6≤42 : [ 6 ] ≤ [ 42 ]
6≤42 = 它
42≤9000 : [ 42 ] ≤ [ 9000 ]
42≤9000 = 它
9000≤+∞ : [ 9000 ] ≤ +∞
9000≤+∞ = 它
下一步:定义查找函数。此函数的结果是不仅是布尔值true/false,而且是证明元素是确实在树上。这是一个证据x个
在树上t吨
是一个证明它是在这里
,证明它在左边
子树,或证明它在正确的
子树。
模块 查找 {{_ : TDO公司 A类}} 哪里
数据 _∈_ {降低} {上面的} (x个 : A类) :
(t吨 : 英国标准时间 A类 降低 上面的) → 设置 哪里
在这里 : ∀ {t₁ 第2} → x个 ≡ 年 → x个 ∈ 节点 年 t₁ 第2
左边 : ∀ {t₁ 第2} → x个 ∈ tõ → x个 ∈ 节点 年 tõ 第2
正确的 : ∀ {t₁ 第2} → x个 ∈ 第2 → x个 ∈ 节点 年 t₁ 第2
定义查找
利用具有
-抽象检查三个
上的函数x个
和年
.
查找 : ∀ {降低} {上面的}
→ (x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的) → 也许 吧 (x个 ∈ t吨)
查找 x个 叶 = 没有什么
查找 x个 (节点 年 tõ 第2) 具有 三个 x个 年
... | 较少的 = 地图可能 左边 (查找 x个 t₁)
... | 平等的 = 只是 (在这里 它)
... | 更大的 = 地图可能 正确的 (查找 x个 第2)
类似地,我们可以定义一个插入函数。这里,我们需要强制要求我们要插入的元素介于边界(或者,我们可以更新返回类型以确保它们包含插入的元素)。
模块 插入 {{_ : TDO公司 A类}} 哪里
插入 : (x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的)
→ {{l≤x : 降低 ≤ [ x个 ]}} {{x≤u : [ x个 ] ≤ 上面的}}
→ 英国标准时间 A类 降低 上面的
插入 x个 叶 = 节点 x个 叶 叶
插入 x个 (节点 年 t₁ 第2) 具有 三个 x个 年
... | 较少的 = 节点 年 (插入 x个 t₁) 第2
... | 平等的 = 节点 年 t₁ 第2
... | 更大的 = 节点 年 t₁ (插入 x个 第2)
为了证明插入的正确性,我们必须证明y∈插入x t
相当于x≠y⊎y∈t
.证据插入声音⁄
和插入-完成
有点长,因为有两个元素x个
和年
两者都可以独立在这里
,位于左侧子树中,或在右边的子树中,我们必须区分9种不同的情况。让我知道如果你能找到一个更短的证据!
打开 查找
插入声音 :
(x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的)
→ {{_ : 降低 ≤ [ x个 ]}} {{_ : [ x个 ] ≤ 上面的}}
→ (x个 ≡ 年) ⊎ (年 ∈ t吨) → 年 ∈ 插入 x个 t吨
插入声音 x个 t吨 (输入 回流) = 插入声音₁ x个 t吨
哪里
插入声音₁ :
(x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的)
→ {{_ : 降低 ≤ [ x个 ]}} {{_ : [ x个 ] ≤ 上面的}}
→ x个 ∈ 插入 x个 t吨
插入声音₁ x个 叶 = 在这里 回流
插入声音₁ x个 (节点 年 t₁ 第2) 具有 三个 x个 年
插入声音₁ x个 (节点 年 t₁ 第2) | 较少的 = 左边 (插入声音₁ x个 t₁)
插入声音₁ x个 (节点 年 t₁ 第2) | 平等的 = 在这里 它
插入声音₁ x个 (节点 年 t₁ 第2) | 更大的 = 正确的 (插入声音₁ x个 第2)
插入声音 x个 t吨 (内部 y∈t) = 插入声音⁄ x个 t吨 y∈t
哪里
插入声音 :
(x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的)
→ {{_ : 降低 ≤ [ x个 ]}} {{_ : [ x个 ] ≤ 上面的}}
→ 年 ∈ t吨 → 年 ∈ 插入 x个 t吨
插入声音⁄ x个 (节点 年 t₁ 第2) (在这里 回流) 具有 三个 x个 年
... | 较少的 = 在这里 回流
... | 平等的 = 在这里 回流
... | 更大的 = 在这里 回流
插入声音⁄ x个 (节点 年 t₁ 第2) (左边 y∈t₁) 具有 三个 x个 年
... | 较少的 = 左边 (插入声音⁄ x个 t₁ y∈t∈)
... | 平等的 = 左边 y∈t₁
... | 更大的 = 左边 y∈t₁
插入声音⁄ x个 (节点 年 t₁ 第2) (正确的 y∈t⁄) 具有 三个 x个 年
... | 较少的 = 正确的 y∈t⁄
... | 平等的 = 正确的 y∈t⁄
... | 更大的 = 正确的 (插入声音⁄ x个 第2 y∈t⁄)
插入-完成 :
(x个 : A类) (t吨 : 英国标准时间 A类 降低 上面的)
→ {{_ : 降低 ≤ [ x个 ]}} {{_ : [ x个 ] ≤ 上面的}}
→ 年 ∈ 插入 x个 t吨 → (x个 ≡ 年) ⊎ (年 ∈ t吨)
插入-完成 x个 叶 (在这里 回流) = 英制 回流
插入-完成 x个 (节点 年 t₁ 第2) y∈t' 具有 三个 x个 年
插入-完成 x个 (节点 年 t₁ 第2) (在这里 回流) | 较少的 = 内部收益率 (在这里 回流)
插入完成 x个 (节点 年 t₁ 第2) (在这里 回流) | 平等的 = 英制 它
插入-完成 x个 (节点 年 t₁ 第2) (在这里 回流) | 更大的 = 内部 (在这里 回流)
插入-完成 x个 (节点 年 t₁ 第2) (左边 y∈t₁') | 较少的 = 地图输入 左边 (插入-完成 x个 t₁ y∈t₁')
插入-完成 x个 (节点 年 t₁ 第2) (左边 y∈t₁) | 平等的 = 内部 (左边 y∈t₁)
插入-完成 x个 (节点 年 t₁ 第2) (左边 y∈t₁) | 更大的 = 内部 (左边 y∈t₁)
插入-完成 x个 (节点 年 t₁ 第2) (正确的 y∈t⁄) | 较少的 = 内部 (正确的 y∈t⁄)
插入-完成 x个 (节点 年 t₁ 第2) (正确的 y∈t∈) | 平等的 = 内部收益率 (正确的 y∈t⁄)
插入-完成 x个 (节点 年 t₁ 第2) (正确的 y∈t⁄') | 更大的 = 地图输入 正确的 (插入-完成 x个 第2 y∈t⁄')
当然,在搜索树上还有很多我们想要的功能实现并证明正确:删除、合并、展平…同样,我们可能希望在中强制执行其他不变量类型,如平衡良好。我强烈建议阅读Conor McBride的纸张在主题,或亲自尝试!