理解 Agda 的语法

Understanding the syntax of Agda

以下面为例

postulate DNE : {A : Set} → ¬ (¬ A) → A

data ∨ (A B : Set) : Set where
  inl : A → A ∨ B
  inr : B → A ∨ B

-- Use double negation to prove exclude middle

classical-2 : {A : Set} → A ∨ ¬ A
classical-2 = λ {A} → DNE (λ z → z (inr (λ x → z (inl x)))

我知道这是正确的,纯粹是因为 agda 的工作原理,但我是这门语言的新手,无法理解它的语法是如何工作的,如果有人能告诉我发生了什么,我将不胜感激上,谢谢:)

我有 haskell 的经验,虽然那是大约一年前的事了。

让我们从假设开始。语法很简单:

postulate name : type

这断言存在一些名为 nametype 类型的值。将其视为逻辑中的公理 - 被定义为真实且不受质疑的事物(在这种情况下由 Agda 提出)。


接下来是数据定义。 mixfix 声明有一个轻微的疏忽,所以我会修复它并解释它的作用。第一行:

data _∨_ (A B : Set) : Set where

引入了一个名为 _∨_ 的新类型(构造函数)。 _∨_ 接受两个 Set 类型的参数,然后 returns 一个 Set.

我会将其与 Haskell 进行比较。 AB 或多或少等同于以下示例中的 ab

data Or a b = Inl a | Inr b

这意味着数据定义定义了多态类型(模板或泛型,如果您愿意的话)。 Set 是 Haskell 的 *.

的 Agda 等价物

下划线是怎么回事? Agda 允许您定义任意运算符(前缀、后缀、中缀……通常只是用一个名称调用 - mixfix)。下划线只是告诉 Agda 参数在哪里。这最好用 prefix/postfix 运算符看到:

-_ : Integer → Integer   -- unary minus
- n = 0 - n

_++ : Integer → Integer   -- postfix increment
x ++ = x + 1

您甚至可以创建疯狂的运算符,例如:

if_then_else_ : ...

下一部分是数据构造函数本身的定义。如果您看过 Haskell 的 GADT,这或多或少是一回事。如果你还没有:

当你在 Haskell 中定义构造函数时,比如上面的 Inr,你只需指定参数的类型,然后 Haskell 计算出整个事物的类型,即Inr :: b -> Or a b。当你在 Agda 中编写 GADT 或定义数据类型时,你需要指定整个类型(这是有充分理由的,但我现在不会深入)。

因此,数据定义指定了两个构造函数,A → A ∨ B 类型的 inlB → A ∨ B 类型的 inr


有趣的部分来了:classical-2 的第一行是一个简单的类型声明。 Set 是怎么回事?当你在Haskell中写多态函数时,你只是用小写字母来表示类型变量,比如:

id :: a -> a

你真正的意思是:

id :: forall a. a -> a

而你真正的意思是:

id :: forall (a :: *). a -> a

即它不仅仅是任何一种 a,而是 a 是一种类型。 Agda 让你做这个额外的步骤并显式地声明这个量化(那是因为你可以量化更多的东西而不仅仅是类型)。

还有花括号?让我再次使用上面的 Haskell 示例。当你在某处使用 id 函数时,比如 id 5,你不需要指定 a = Integer.

如果您使用正常的括号,则每次调用 classical-2 时都必须提供实际类型 A。但是,大多数时候,类型可以从上下文中推导出来(很像上面的 id 5 示例),因此对于这些情况,您可以 "hide" 参数。然后 Agda 尝试自动填充它 - 如果它不能,它会抱怨。


最后一行:λ x → y 是 Agda 表达 \x -> y 的方式。这应该可以解释大部分内容,唯一剩下的就是花括号了。我很确定你可以在这里省略它们,但无论如何:隐藏的参数做他们所说的 - 他们隐藏。因此,当您定义从 {A}B 的函数时,您只需提供 B 类型的内容(因为 {A} 是隐藏的)。在某些情况下,您需要知道隐藏参数的值,这就是这种特殊类型的 lambda 的作用:λ {A} → 允许您访问隐藏的 A!