向你学习Agda(2014)

2022-02-17 04:33:31

这是一个降价版的教程学习你的Agda和实现启蒙!利亚姆·奥康纳·戴维斯著。

我制作这个版本是为了我自己的参考,同时我还进行了一些修改和补充,以及一些修正。你可能更喜欢原版。

欢迎学习Agda并获得启发!如果你正在读这篇文章,你可能会好奇Agda是什么,为什么你想学习它,以及一般来说,依赖类型的纯函数编程有什么大不了的。

受BONUS的启发,我决定写一本易懂的Agda教程,向普通人而不是象牙塔学者介绍独立打字编程。当然,鉴于我是象牙塔学者之一,这可能并不容易。不过,我准备试一试。学习Agda对我来说是一个非常有益但非常困难的过程。我希望,通过编写本教程,每个人都能轻松一点。

(最初的教程建议在Agda之前先学习Haskell。但是,如果一个人已经知道一些函数式编程,那么Agda应该不是很难学。对于那些具有一定逻辑背景和其他依赖类型语言经验的人来说,这一点尤其重要。)

本教程并非针对那些完全不熟悉功能编程的人。Agda在基本层面上类似于Haskell和ML等类型化函数语言,因此了解ML家族中的一种语言肯定会使学习Agda变得更容易。

如果您不懂静态类型的函数式语言,我建议您学习Haskell,因为Agda与Haskell生态系统有着密切的关系。如果你想找一本好的哈斯凯尔教程,那就看看这本书的比较吧,给你学一本哈斯凯尔。

如果你不知道纯粹的函数式编程是如何工作的,在尝试解决Agda之前先学一点。

了解命令式和面向对象编程(C、Java、Ruby等)没有必要。事实上,当你试图学习Agda时,尝试运用从这些语言中学习到的技能甚至可能会表现得很糟糕。

这个故事的寓意是:保持开放的心态。Agda的很多功能最初很难理解。过了很长一段时间,阿格达的一切才在我的脑海中就位。阿格达很难。然而,过了一段时间,阿格达的内在美脱颖而出,一切都只是咔哒一声。如果你在Agda学习中遇到障碍,不要气馁!继续努力,最终你会成为阿格达夫的大师。

Agda是一种编程语言,但不像Java那样是一种编程语言。它甚至不太像Haskell,尽管它更像Haskell而不是Java。

Agda是一种使用依赖类型的编程语言。很多人都会熟悉java语言或C++等命令式语言的类型,如果你重新阅读到这一点,你也应该熟悉Haskel.

这些语言中的类型本质上是用标记来注释表达式。在一个简单的层次上,表达式的类型可能只是一个具体类型,比如BoL或int(java)(泛型)、C++(通过模板)和Haskell都支持多态类型,例如列表A或图K V。

但是,如果列表a是一种类型,那么什么才是列表(没有参数)?Haskell称之为“类型构造函数”,但实际上它是类型级别的函数。List接受一个类型,比如Int,然后返回一个新类型ListInt。Haskell(带有适当的扩展)甚至支持类型级别上的任意函数,这些函数不一定要构造类型术语,而Instead可以简单地引用现有的函数。

因此,Haskell有类型级别的函数,甚至有类型级别的类型(种类)。它几乎像是一种全新的语言,覆盖在Haskell之上,在编译时运行,处理类型术语。

事实上,你可以这样想任何类型的系统。在C++中,人们利用其类型系统的完整性来完成编译时分析和计算。虽然这种打字水平的工作非常强大,但我担心这种打字机器往往很难理解和操作。即使在Haskell中,广泛使用类型级计算的应用程序通常也很难理解。类型级别的“语言”几乎总是比值级别的“语言”更复杂

在Agda中,类型和值之间不存在区别。相反,用于操纵类型术语的语言与用于操纵值的语言完全相同。

这意味着您实际上可以在类型中包含值。例如,Listtype构造函数可以通过其内容的类型和相关列表的长度进行参数化(我们将在后面进行)。例如,这允许编译器检查您,以确保您没有试图在一个可能为空的列表上调用head的情况。能够在类型中包含值,并对其使用所有相同的值级操作,这就是Agda依赖于类型的原因——不仅值可以有一个类型,而且类型也可以有一个值。

事实上,由于值的语言和类型的语言是相同的,所以任何可以表达的关于值的属性都可以在其类型中静态地表达,并由Agda进行机器检查。我们可以从程序中静态消除任何错误场景。

如果我能想出一个Foo类型的函数->;Bar(Agda说它是正确的)这意味着我不仅编写了一个程序,而且还编写了一个证明结构,假设某个前提为Foo,判断Barhold。(稍后我们将更多地讨论证据;我不想现在就陷入细节。)

鉴于我们的Foo和Bar可以随心所欲地表达,这让我们只需利用证明和程序之间的这种对应关系,就可以对我们的程序进行任何我们想要的改进——称为Curry Howard对应关系,这是两位杰出的逻辑学家在60年代发现的。

软件形式验证的有效性经常受到程序员的激烈质疑,他们通常没有正式验证的经验。通常,测试方法被认为是一种更可行的选择。

虽然在某些可以接受bug的情况下,形式验证是过度的,但我几乎不认为测试可以完全取代形式验证。以下是三个原因:

证明在并行场景中有效。您无法可靠地针对竞争条件、饥饿或死锁进行单元测试。所有这些都可以通过形式化方法消除。

像程序一样,证明也是合成的。测试并非如此。在测试场景中,通常必须编写单元测试和集成测试:分别测试小组件的单元测试,以及测试这些小组件之间交互的集成测试。如果我有这些小组件行为的证明,我可以简单地使用这些证明结果来满足关于它们的交互的证明义务——没有必要为两种测试场景重新创造一切。

证据是傻瓜式的。如果我有一套测试来显示某些属性,那么该属性可能实际上并不成立——我只是在测试中不够彻底。有了正式的验证,违反你的财产就不可能像那样从裂缝中溜走。

当然,证据并非适用于所有情况,但我认为它们应该比目前更广泛地使用。

多亏了Curry Howard,Agda还可以用作证明语言,而不是编程语言。你可以构建一个证明,不仅是关于你的程序,而且是关于你喜欢的任何东西。

事实上,库里·霍华德向我们展示了函数式编程(Lambda演算)的基本原理和数学证明(逻辑)的基本原理实际上是一样的(同构的)。这意味着我们可以在Agda中以程序的形式构造数学证明,并让Agda为我们检查它们。它必须和标准的纸笔数学证明一样有效(可能更有效,因为Agda不允许我们把任何东西作为“读者练习”留下”——Agda可以自动为我们检查证明的正确性。我们稍后将通过证明一些关于Peano自然数的基本数学性质来完成这项工作。

因此,阿格达是一种真正实现了库里·霍华德梦想的语言。Agda程序也是inits类型表示的公式的证明。

在编写本文时,只有使用ingemacs编辑Agda代码才是真正可行的。GNU Emacs或XEmacs都可以。然而,编辑Agda代码并不需要精通Emacs。

(如果您使用的是Ubuntu Linux,您可能希望跳到下一节,在Ubuntu Linux上安装。)

您将需要GHC、Haskell编译器以及各种工具和库,它们构成了Haskell平台。这是开始使用Haskell的最佳方式,也是获得Agda的最简单方式。

一旦你有了Haskell和Emacs,你还需要做三件事:

安装Agda。Linux用户可以从packagemanager中获得Agda软件包(搜索“Agda”以查找)。如果没有或没有,只需使用Haskell平台的cabal安装工具下载、编译和设置Agda即可。

为emacs安装Agda模式。只需输入命令提示符(Agda在您的路径中):

还要编译Agda模式(如果更新Agda,则需要再次编译):

到那时你应该准备好了。要想知道一切是否都如预期般顺利,请转到下一节。

在Ubuntu Linux系统上,不必像上面那样使用cabal安装Agda,也可以使用以下两个命令,这两个命令需要几分钟才能运行:

就这样!现在,当您启动Emacs并使用。agda扩展,应切换至agda模式或agda2模式。如果没有,您可以通过调用以下选项之一(当然是在Emacs中)手动切换:M-x agda模式或M-x agda2模式或Esc-x agda模式。(找到可用AGDAMODE的一个简单方法是键入M-x agda,然后点击tab几次,查看可能的完成情况。)

大多数语言教程都以典型的“Hello,World”示例开头,但这并不适用于Agda中的第一个示例。与其他语言不同,Agda依赖于大量的原始操作和基本构造的特殊情况,Agda非常小,大多数“语言构造”实际上都是在库中定义的。

Agda甚至没有内置数字,所以我们要做的第一件事就是定义它们,特别是自然数。自然数是非负整数,也就是以零和goingup开头的整数。数学使用符号$\N$来表示自然数,因此我们将借用它作为例子(Agda与其他语言不同的另一个特点是它广泛使用unicode,使数学构造更自然)。进入ℕ 在emacs中,键入\bn。输入unicode箭头(→),键入\->;。我将一行一行地演示这一点,请耐心听我说。

data关键字表示我们在本例中定义了一个类型,ℕ.在本例中,我们指定ℕ 是Set类型(冒号的意思)。

如果您还记得介绍的话,我提到在Agda中,类型和值的处理方式是相同的。因为值是给定的类型,所以类型也是给定的类型。类型只是一组特殊的语言术语,在Agda中,所有术语都有类型。

甚至设置(我们的类型)ℕ) 有一种类型:Set₁, 哪个有一套字体₂, 一直到无穷远。稍后我们将进一步讨论这些集合类型的含义,但现在您可以将集合视为我们在程序中使用的所有数据类型的类型。

这种类型的无限层次结构为罗素悖论提供了一个优雅的解决方案。例如,设置₁ 不能包含集合₁ 还是设定₂, 只有集合,所以罗素的问题集合(包含自身)不可能存在。

好的,我们已经定义了类型,但是现在我们需要用值填充类型。虽然没有值的类型确实有它的用途,但没有值的自然数类型是绝对错误的。所以,我们要定义的第一个自然数是零:

在这里,我们只是宣布零这个术语是我们新类型的一个成员ℕ. 我们可以继续这样定义更多的数字:

但是,我们很快就会发现我们的文本编辑器中充满了定义,而且我们定义所有自然数的速度也不会比我们开始时快。所以,我们应该引用一个严格的数学定义。

对于任何自然数$n$,$n+1$也是一个自然数。为方便起见,我们将$n+1$称为$\mathtt{suc}\n.$1($\forall n\in\mathbb{n}.\\mathtt{suc}\n\in\mathbb{n}$)。

(我在这里使用的符号应该对任何了解集合论和/或一阶逻辑的人都很熟悉。不要惊慌。如果你不了解这些东西,我们将在AGDA中为类似的东西开发模型,这样你就可以在我们继续的过程中学习。)

这被称为自然数的归纳定义。我们称之为归纳,是因为它由一个基本规则和一个归纳规则组成,在这个基本规则中,我们定义了一个固定的起点,而归纳规则应用于集合中的一个元素时,会归纳出集合中的下一个元素。这是一种非常优雅的定义大集合的方式。这种定义自然数的方法是由一位名叫朱塞佩·皮亚诺(Giuseppe Peano)的数学学家发明的,因此它们被称为皮亚诺数。

在接下来的章节中,我们将研究归纳证明,它具有相似的结构。

对于基本情况,我们已经在$\mathbb{N}$中定义了零,方法是:零:ℕ. 为了归纳地定义自然数,让我们回顾一下一阶逻辑的诱导步骤。这可以写如下:

$\forall n\in\mathbb{n}.\\mathtt{suc}\n\in\mathbb{n}$给定一个自然数n,构造函数suc将返回另一个自然数。换句话说,suc可以被认为是一个函数,当给定一个自然数时,它会产生下一个自然数。在Agda中,我们这样定义构造函数suc:

现在我们可以将数字1表示为suc zero,将数字2表示为suc(suczero),将数字3表示为suc(suc(suc zero)),依此类推。

如果您将其加载到GHCi中,并询问它Suc的类型,它(毫不奇怪)会告诉您:Nat->;纳特。这是学习如何在Agda中定义构造函数的好方法。

此外,GHC还支持一个扩展,即广义代数数据类型或GADT,它允许您定义Agda样式的数据类型:

值得注意的是,GADT与Agda数据定义并不完全相同,Haskell仍然不是依赖于类型的,因此您在本书中学到的很多内容不会直接延续到扩展的Haskell。

现在,我们将对自然数定义一些算术运算。让我们先试试加法。

我在这里声明一个函数。首先,我给它一个类型2,它接受两个自然数,然后返回一个自然数。

与Haskell不同,它只有前缀函数(普通函数)和InfixFunction(运算符),Agda支持mixfix语法。这允许您声明参数可以出现在术语中任何位置的函数。你可以用下划线来指代论点的“漏洞”。

这可以非常灵活地使用:你可以用if-athen b else c调用这个函数,Agda将其解释为if_-then_else_uuB c。这种语法灵活性提供了强大的表达能力,但要小心过度使用,因为它可能会让人非常困惑!

_+_ : ℕ → ℕ → ℕ 零+m=m(suc n)+m=suc(n+m)

通常情况下,我们会在此时运行程序,以验证它是否有效,但在Agdawe中,请检查我们的代码。这将检查我们的所有证明义务是否已得到履行:

它会检查你的类型。类型是在Agda中对证明进行编码的方式(尽管我们还没有做任何非琐碎的证明),所以这一点很重要。

程序的证明义务只能在程序终止时进行机器检查,但检查任何程序是否终止通常是不可判定的(请参阅停止问题)。为了避免这种困境,Agda只在有限数据结构的结构递归上运行它的检查器,并警告它不能检查使用非结构递归的证明义务。我们将在后面的章节中对此进行更多讨论,但Agda可以证明本教程早期的所有示例都是终止的。

模块LearnYouAn数据在哪里ℕ : 设置为零:ℕ suc:ℕ → ℕ _+_ : ℕ → ℕ → ℕ 零+m=m(suc n)+m=suc(n+m)

确保缩进正确!特别是,zero和suc的构造函数从第5列开始(数据中t的下方),而+u定义中的所有三行都从第3列开始。

要检查程序,请在Emacs中键入C-C-l(或从Agdamenu中选择Load)。如果程序检查正确,则不会出现错误消息、孔标记(黄色高亮显示)和橙色高亮显示的非终止部分。此外,单词(Agda:Checked)应该出现在Emacs模式行中,您应该注意到字符的颜色已经改变。

现在,我们的检查并没有那么有意义。他们证明的唯一一件事是,我们的加法函数确实可以接受任何自然数,并产生一个自然数,正如类型所示。后来,当我们在类型中编码更多信息时,我们的检查可能比运行和测试程序意味着更多。

要计算一个表达式(只是为了验证它是否真的有效),我们可以在emacs中键入C-C-n,或者从agdamnus中选择“将术语计算为标准形式”。然后,在minibuffer中,我们可以为3+2输入一个表达式:

在本节中,我们研究了Peano自然数,并在Agda中定义了一些基本函数和数据类型。在下一节中,我们将介绍命题逻辑,以及如何使用该系统在Agda中编码逻辑证明。

如果第一个程序无法运行,请确保缩进正确。下载LearnYouAn可能会有所帮助。agda文件,只是为了确定。

现在我们已经定义了自然数,我们要做一些简单的例子来证明一些基本的数学性质。我们将首先讨论自然演绎中的逻辑和逻辑规范,接下来,我们将讨论Agda的应用。

从根本上讲,逻辑是一个判断系统。判断是数学语言中可能被证明或未被证明的陈述。一种语言通常被描述为一组字符串,这些字符串构成了该语言的每个术语,但这是一种简化:一种语言可以由任意数据结构组成。我们只使用字符串来表示这些结构,因为任何数据结构都可以用字符串形式表示。

在我们的例子中,我们将基于前面使用的自然数$\mathbb{N}$语言定义一个非常简单的逻辑。

我们只需要一种类型的判断,形式为$\mathbb{N}\\textbf{偶数}$,只有当给定的数字是偶数时才可以证明。

逻辑由一组公理和一组规则组成。公理是逻辑的基础:它们是基本的、简单的、被认为是正确的陈述。规则描述了如何从现有的定理中产生新的定理。定理是一个已被证明的命题。因此,规则告诉我们如何使用其他语句的证明来构造语句的证明。我们可以通过将这些公理和规则以推理规则的形式写入名为naturaldeduction的元逻辑中,从而正式指定这些公理和规则,如下所示:

这就是说,如果我们能证明所有的前提$P_1\cdots P_n$,那么我们就能证明结论$C$。

出于我们的目的,我们只有一条公理,那就是零是偶数。公理是作为推理规则编写的,没有前提:

然后,基于

......