协助Agda的终止检查

时间:2013-10-28 18:58:36

标签: functional-programming termination agda

假设我们定义了一个函数

f : N \to N
f 0 = 0
f (s n) = f (n/2) -- this / operator is implemented as floored division.

Agda会在三文鱼中画f,因为它无法判断n / 2是否小于n。我不知道怎么告诉Agda的终止检查器。我在标准库中看到它们有一个2的分层和n / 2< ñ。但是,我仍然没有看到如何让终止检查器意识到在较小的子问题上进行了递归。

6 个答案:

答案 0 :(得分:20)

Agda的终止检查器只检查结构递归(即在结构上较小的参数上发生的调用),并且没有办法确定某个关系(例如_<_)暗示其中一个参数在结构上较小。 / p>


题外话:积极性检查会发生类似的问题。考虑标准的定点数据类型:

data μ_ (F : Set → Set) : Set where
  fix : F (μ F) → μ F

Agda拒绝这一点,因为F在其第一个参数中可能不是正面的。但是我们不能将μ限制为仅采用正类型函数,或者显示某些特定类型函数是正数。


我们通常如何显示递归函数终止?对于自然数,这是事实,如果递归调用发生在严格较小的数字上,我们最终必须达到零并且递归停止;对于列表,其长度相同;对于集合,我们可以使用严格的子集关系;等等。请注意,“严格较小的数字”不适用于整数。

所有这些关系所共有的财产被称为有根据的。非正式地说,如果一个关系没有任何无限的下行链,那么它就是有根据的。例如,自然数的<是有根据的,因为对于任何数字n

n > n - 1 > ... > 2 > 1 > 0

也就是说,这种链的长度受n + 1的限制。

然而,关于自然数的

没有充分根据:

n ≥ n ≥ ... ≥ n ≥ ...

整数都不是<

n > n - 1 > ... > 1 > 0 > -1 > ...

这对我们有帮助吗?事实证明,我们可以编码关系在Agda中有充分根据的意义,然后用它来实现你的功能。

为简单起见,我要将_<_关系烘焙到数据类型中。首先,我们必须定义一个数字可供访问的含义:如果n所有m也可访问,则m < n可访问。这当然会在n = 0停止,因为没有m所以m < 0data Acc (n : ℕ) : Set where acc : (∀ m → m < n → Acc m) → Acc n 这个陈述很简单。

<

现在,如果我们可以证明所有自然数都可以访问,那么我们就证明acc是有根据的。为什么会这样?必须有一定数量的<构造函数(即没有无限下行链),因为Agda不会让我们写无限递归。现在,似乎我们只是将问题推回了一步,但写出有根据的证据实际上是结构递归的!

所以,考虑到这一点,这里有WF : Set WF = ∀ n → Acc n 有充分根据的定义:

<-wf : WF
<-wf n = acc (go n)
  where
  go : ∀ n m → m < n → Acc m
  go zero    m       ()
  go (suc n) zero    _         = acc λ _ ()
  go (suc n) (suc m) (s≤s m<n) = acc λ o o<sm → go n o (trans o<sm m<n)

有充分根据的证明:

go

请注意trans在结构上很好地递归。 open import Data.Nat open import Relation.Binary open DecTotalOrder decTotalOrder using (trans) 可以像这样导入:

⌊ n /2⌋ ≤ n

接下来,我们需要一个/2-less : ∀ n → ⌊ n /2⌋ ≤ n /2-less zero = z≤n /2-less (suc zero) = z≤n /2-less (suc (suc n)) = s≤s (trans (/2-less n) (right _)) where right : ∀ n → n ≤ suc n right zero = z≤n right (suc n) = s≤s (right n)

的证明
f

最后,我们可以编写您的Acc函数。请注意,由于acc,它突然变得结构递归:递归调用发生在一个f : ℕ → ℕ f n = go _ (<-wf n) where go : ∀ n → Acc n → ℕ go zero _ = 0 go (suc n) (acc a) = go ⌊ n /2⌋ (a _ (s≤s (/2-less _))) 构造函数剥离的参数上。

Acc

现在,必须直接与Acc合作并不是很好。这就是Dominique的答案所在。我在这里写的所有这些东西都已经在标准库中完成了。它更通用(<-rec数据类型实际上是通过关系进行参数化的),它允许您只使用Acc而不必担心A


更仔细地看,我们实际上非常接近通用解决方案。让我们看看当我们对关系进行参数化时我们得到了什么。为简单起见,我不处理宇宙多态性。

A上的关系只是一个函数,需要两个Set并返回Rel : Set → Set₁ Rel A = A → A → Set (我们可以称之为二元谓词):

Acc

我们可以通过将硬编码的_<_ : ℕ → ℕ → Set更改为某种类型A的任意关系来轻松概括data Acc {A} (_<_ : Rel A) (x : A) : Set where acc : (∀ y → y < x → Acc _<_ y) → Acc _<_ x

WellFounded : ∀ {A} → Rel A → Set
WellFounded _<_ = ∀ x → Acc _<_ x

有根本的定义会相应地改变:

Acc

现在,由于foldr是一种与其他类似的归纳数据类型,我们应该能够编写它的消除器。对于归纳类型,这是一个折叠(很像foldAccSimple : ∀ {A} {_<_ : Rel A} {R : Set} → (∀ x → (∀ y → y < x → R) → R) → ∀ z → Acc _<_ z → R foldAccSimple {R = R} acc′ = go where go : ∀ z → Acc _ z → R go z (acc a) = acc′ z λ y y<z → go y (a y y<z) 列表的消除器) - 我们告诉消除器如何处理每个构造函数的情况,并且消除器将其应用于整个结构。

在这种情况下,我们可以使用简单的变体做好:

_<_

如果我们知道Acc _<_ z是有充分根据的,我们可以完全跳过recSimple : ∀ {A} {_<_ : Rel A} → WellFounded _<_ → {R : Set} → (∀ x → (∀ y → y < x → R) → R) → A → R recSimple wf acc′ z = foldAccSimple acc′ z (wf z) 参数,所以让我们写一些小便利包装器:

<-wf : WellFounded _<_
<-wf = {- same definition -}

<-rec = recSimple <-wf

f : ℕ → ℕ
f = <-rec go
  where
  go : ∀ n → (∀ m → m < n → ℕ) → ℕ
  go zero    _ = 0
  go (suc n) r = r ⌊ n /2⌋ (s≤s (/2-less _))

最后:

foldAcc : ∀ {A} {_<_ : Rel A} (P : A → Set) →
          (∀ x → (∀ y → y < x → P y) → P x) →
          ∀ z → Acc _<_ z → P z
foldAcc P acc′ = go
  where
  go : ∀ z → Acc _ z → P z
  go _ (acc a) = acc′ _ λ _ y<z → go _ (a _ y<z)

rec : ∀ {A} {_<_ : Rel A} → WellFounded _<_ →
      (P : A → Set) → (∀ x → (∀ y → y < x → P y) → P x) →
      ∀ z → P z
rec wf P acc′ z = foldAcc P acc′ _ (wf z)

事实上,这看起来(和工作)几乎就像标准库中的那个!


如果你想知道,这是完全相关的版本:

{{1}}

答案 1 :(得分:13)

我想提供一个与上面给出的答案略有不同的答案。特别是,我想建议,而不是试图以某种方式说服终止检查器,实际上,不,这个递归是完全正常的,我们应该尝试重新建立有根据的ness,以便递归显然是好的,因为结构化。

这里的想法是问题来自于无法看到n / 2在某种程度上是n的“部分”。结构递归想要将一个东西分解为它的直接部分,但n / 2n的“部分”的方式是我们放弃所有其他suc。但事先并不明显有多少下降,我们必须四处寻找并尝试排队。如果我们有一些具有“多个”suc s的构造函数的类型,那会更好。

为了使问题稍微有趣,让我们尝试定义行为类似于

的函数
f : ℕ → ℕ
f 0 = 0
f (suc n) = 1 + (f (n / 2))

也就是说,应该是

的情况
f n = ⌈ log₂ (n + 1) ⌉

现在自然上面的定义不起作用,原因与f不相同。但是让我们假装它做了,让我们探索“道路”,可以说,这个论点将通过自然数字。假设我们查看n = 8

f 8 = 1 + f 4 = 1 + 1 + f 2 = 1 + 1 + 1 + f 1 = 1 + 1 + 1 + 1 + f 0 = 1 + 1 + 1 + 1 + 0 = 4

所以“路径”是8 -> 4 -> 2 -> 1 -> 0。比方说,11?

f 11 = 1 + f 5 = 1 + 1 + f 2 = ... = 4

所以“路径”是11 -> 5 -> 2 -> 1 -> 0

很自然地,这里发生的是,在每一步我们要么除以2,要么减去1并除以2.每个自然数大于0都可以这种方式唯一地分解。如果它是偶数,则除以2并继续,如果它是奇数,则减1并除以2然后继续。

现在我们可以确切地看到我们的数据类型应该是什么样子。我们需要一个具有构造函数的类型,意味着“两倍suc”,另一个意味着“两倍suc加一”,当然还有一个构造函数意味着“零suc s”:

data Decomp : ℕ → Set where
  zero : Decomp zero
  2*_ : ∀ {n} → Decomp n → Decomp (n * 2)
  2*_+1 : ∀ {n} → Decomp n → Decomp (suc (n * 2))

我们现在可以定义将自然数分解为与其对应的Decomp的函数:

decomp : (n : ℕ) → Decomp n
decomp zero = zero
decomp (suc n) = decomp n +1

+1 s:

定义Decomp会很有帮助
_+1 : {n : ℕ} → Decomp n → Decomp (suc n)
zero +1 = 2* zero +1
(2* d) +1 = 2* d +1
(2* d +1) +1 = 2* (d +1)

给定Decomp,我们可以将其展平为自然数,忽略2*_2*_+1之间的区别:

flatten : {n : ℕ} → Decomp n → ℕ
flatten zero = zero
flatten (2* p) = suc (flatten p)
flatten (2* p +1 = suc (flatten p)

现在定义f

是微不足道的
f : ℕ → ℕ
f n = flatten (decomp n)

这很愉快地通过终止检查器没有问题,因为我们实际上从未对有问题的n / 2进行递归。相反,我们将数字转换为直接以结构递归方式表示数字空间路径的格式。

编辑 我刚才发现Decomp是二进制数的小端表示。 2*_是“向末尾追加0 /向左移1位”,2*_+1是“向末尾追加1 /向左移1位并加1”。所以上面的代码实际上是关于显示二进制数在结构上是递归的,除以2,它们应该是!这让我更容易理解,但我不想改变我已经写过的东西,所以我们可以在这里重新命名:Decomp〜&gt; Binary2*_〜&gt; _,zero2*_+1〜&gt; _,onedecomp〜&gt; natToBinflatten〜&gt; countBits

答案 2 :(得分:9)

在接受Vitus的回答之后,我发现了一种不同的方法来实现证明函数终止于Agda的目标,即使用“大小类型”。我在这里提供我的答案,因为它似乎是可以接受的,也是对这个答案的任何弱点的批评。

描述了大小类型: http://arxiv.org/pdf/1012.4896.pdf

它们在Agda中实现,而不仅仅是MiniAgda;见这里:http://www2.tcs.ifi.lmu.de/~abel/talkAIM2008Sendai.pdf

我们的想法是增加数据类型,其大小允许类型检查器更容易证明终止。大小在标准库中定义。

open import Size

我们定义大小的自然数:

data Nat : {i : Size} \to Set where
    zero : {i : Size} \to Nat {\up i} 
    succ : {i : Size} \to Nat {i} \to Nat {\up i}

接下来,我们定义前驱和减法(monus):

pred : {i : Size} → Nat {i} → Nat {i}
pred .{↑ i} (zero {i}) = zero {i}
pred .{↑ i} (succ {i} n) = n 

sub : {i : Size} → Nat {i} → Nat {∞} → Nat {i}
sub .{↑ i} (zero {i}) n = zero {i}
sub .{↑ i} (succ {i} m) zero = succ {i} m
sub .{↑ i} (succ {i} m) (succ n) = sub {i} m n

现在,我们可以通过Euclid算法定义除法:

div : {i : Size} → Nat {i} → Nat → Nat {i}
div .{↑ i} (zero {i}) n = zero {i}
div .{↑ i} (succ {i} m) n = succ {i} (div {i} (sub {i} m n) n)

data ⊥ : Set where
record ⊤ : Set where
notZero :  Nat → Set
notZero zero = ⊥
notZero _ = ⊤

我们给非零分母的分工。 如果分母非零,则其形式为b + 1。然后我们这样做 divPos a(b + 1)= div a b 由于div a b返回上限(a /(b + 1))。

divPos : {i : Size} → Nat {i} → (m : Nat) → (notZero m) → Nat {i}
divPos a (succ b) p = div a b
divPos a zero ()

作为辅助:

div2 : {i : Size} → Nat {i} → Nat {i}
div2 n = divPos n (succ (succ zero)) (record {})

现在我们可以定义一种用于计算第n个斐波那契数的分而治之法。

fibd : {i : Size} → Nat {i} → Nat
fibd zero = zero
fibd (succ zero) = succ zero
fibd (succ (succ zero)) = succ zero
fibd (succ n) with even (succ n)
fibd .{↑ i}  (succ {i} n) | true = 
  let
    -- When m=n+1, the input, is even, we set k = m/2
    -- Note, ceil(m/2) = ceil(n/2)
    k = div2 {i} n
    fib[k-1] = fibd {i} (pred {i} k)
    fib[k] = fibd {i} k
    fib[k+1] =  fib[k-1] + fib[k]
  in
    (fib[k+1] * fib[k]) + (fib[k] * fib[k-1])
fibd .{↑ i} (succ {i} n) | false = 
  let
    -- When m=n+1, the input, is odd, we set k = n/2 = (m-1)/2.
    k = div2 {i} n
    fib[k-1] = fibd {i} (pred {i} k)
    fib[k] = fibd {i} k
    fib[k+1] = fib[k-1] + fib[k]
  in
    (fib[k+1] * fib[k+1]) + (fib[k] * fib[k])

答案 3 :(得分:5)

您无法直接执行此操作:Agda的终止检查程序仅在语法较小的参数上考虑递归确定。但是,Agda standard library提供了一些模块,用于使用函数参数之间有充分理由的顺序来证明终止。自然数的标准顺序就是这样的顺序,可以在这里使用。

使用Induction。*中的代码,您可以按如下方式编写函数:

open import Data.Nat
open import Induction.WellFounded
open import Induction.Nat

s≤′s : ∀ {n m} → n ≤′ m → suc n ≤′ suc m
s≤′s ≤′-refl = ≤′-refl
s≤′s (≤′-step lt) = ≤′-step (s≤′s lt)

proof : ∀ n → ⌊ n /2⌋ ≤′ n
proof 0 = ≤′-refl
proof 1 = ≤′-step (proof zero)
proof (suc (suc n)) = ≤′-step (s≤′s (proof n))

f : ℕ → ℕ
f = <-rec (λ _ → ℕ) helper
  where
    helper : (n : ℕ) → (∀ y → y <′ n → ℕ) → ℕ
    helper 0 rec = 0
    helper (suc n) rec = rec ⌊ n /2⌋ (s≤′s (proof n))

我发现了一篇有一些解释的文章here。但可能有更好的参考资料。

答案 4 :(得分:2)

几周前在Agda邮件列表上出现了一个类似的问题,似乎是将Data.Nat元素注入Data.Bin,然后对此表示使用结构递归,这很好 - 适合手头的工作。

您可以在此处找到整个帖子:http://comments.gmane.org/gmane.comp.lang.agda/5690

答案 5 :(得分:2)

您可以避免使用有根据的递归。我们假设你想要一个函数,它将⌊_/2⌋应用于一个数字,直到达到0,然后收集结果。使用{-# TERMINATING #-}编译指示,可以像这样定义:

{-# TERMINATING #-}
⌊_/2⌋s : ℕ -> List ℕ
⌊_/2⌋s 0 = []
⌊_/2⌋s n = n ∷ ⌊ ⌊ n /2⌋ /2⌋s

第二个条款相当于

⌊_/2⌋s n = n ∷ ⌊ n ∸ (n ∸ ⌊ n /2⌋) /2⌋s

通过内联减法可以使⌊_/2⌋s在结构上递归:

⌊_/2⌋s : ℕ -> List ℕ
⌊_/2⌋s = go 0 where
  go : ℕ -> ℕ -> List ℕ
  go  _       0      = []
  go  0      (suc n) = suc n ∷ go (n ∸ ⌈ n /2⌉) n
  go (suc i) (suc n) = go i n

go (n ∸ ⌈ n /2⌉) ngo (suc n ∸ ⌊ suc n /2⌋ ∸ 1) n

的简化版本

一些测试:

test-5 : ⌊ 5 /2⌋s ≡ 5 ∷ 2 ∷ 1 ∷ []
test-5 = refl

test-25 : ⌊ 25 /2⌋s ≡ 25 ∷ 12 ∷ 6 ∷ 3 ∷ 1 ∷ []
test-25 = refl

现在让我们假设你想要一个函数,它将⌊_/2⌋应用于一个数字,直到达到0,然后对结果求和。它只是

⌊_/2⌋sum : ℕ -> ℕ
⌊ n /2⌋sum = go ⌊ n /2⌋s where
  go : List ℕ -> ℕ
  go  []      = 0
  go (n ∷ ns) = n + go ns

因此我们可以在列表上运行递归,该列表包含由⌊_/2⌋s函数生成的值。

更简洁的版本是

⌊ n /2⌋sum = foldr _+_ 0 ⌊ n /2⌋s

回到了良好的基础。

open import Function
open import Relation.Nullary
open import Relation.Binary
open import Induction.WellFounded
open import Induction.Nat

calls : ∀ {a b ℓ} {A : Set a} {_<_ : Rel A ℓ} {guarded : A -> Set b}
      -> (f : A -> A)
      -> Well-founded _<_
      -> (∀ {x} -> guarded x -> f x < x)
      -> (∀ x -> Dec (guarded x))
      -> A
      -> List A
calls {A = A} {_<_} f wf smaller dec-guarded x = go (wf x) where
  go : ∀ {x} -> Acc _<_ x -> List A
  go {x} (acc r) with dec-guarded x
  ... | no  _ = []
  ... | yes g = x ∷ go (r (f x) (smaller g))

此函数与⌊_/2⌋s函数的作用相同,即为递归调用生成值,但对于满足某些条件的任何函数。

查看go的定义。如果x不是guarded,则返回[]。否则先添加x并在go上致电f x(我们可以写go {x = f x} ...),这在结构上要小一些。

我们可以根据⌊_/2⌋s

重新定义calls
⌊_/2⌋s : ℕ -> List ℕ
⌊_/2⌋s = calls {guarded = ?} ⌊_/2⌋ ? ? ?

⌊ n /2⌋s仅在[]n时返回0,因此guarded = λ n -> n > 0

我们有根据的关系基于_<′_,并在Induction.Nat模块中定义为<-well-founded

所以我们有

⌊_/2⌋s = calls {guarded = λ n -> n > 0} ⌊_/2⌋ <-well-founded {!!} {!!}

下一个洞的类型是{x : ℕ} → x > 0 → ⌊ x /2⌋ <′ x

我们可以很容易地证明这个命题:

open import Data.Nat.Properties

suc-⌊/2⌋-≤′ : ∀ n -> ⌊ suc n /2⌋ ≤′ n
suc-⌊/2⌋-≤′  0      = ≤′-refl
suc-⌊/2⌋-≤′ (suc n) = s≤′s (⌊n/2⌋≤′n n)

>0-⌊/2⌋-<′ : ∀ {n} -> n > 0 -> ⌊ n /2⌋ <′ n
>0-⌊/2⌋-<′ {suc n} (s≤s z≤n) = s≤′s (suc-⌊/2⌋-≤′ n)

最后一个洞的类型是(x : ℕ) → Dec (x > 0),我们可以通过_≤?_ 1填充。

最终的定义是

⌊_/2⌋s : ℕ -> List ℕ
⌊_/2⌋s = calls ⌊_/2⌋ <-well-founded >0-⌊/2⌋-<′ (_≤?_ 1)

现在,您可以递归⌊_/2⌋s生成的列表,而不会出现任何终止问题。