繁体   English   中英

协助Agda的终止检查

[英]Assisting Agda's termination checker

假设我们定义了一个函数

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 <n的证明。 但是,我仍然没有看到如何让终止检查器意识到已经在较小的子问题上进行了递归。

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


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

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

阿格达拒绝这一点,因为F的第一个论点可能不是正面的。 但是我们不能将μ限制为仅采用正类型函数,或者表明某些特定类型函数是正数。


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

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

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

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

然而, 自然数字并不是很有根据:

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

并且< on整数:

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

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

为简单起见,我要将_<_关系烘焙到数据类型中。 首先,我们必须定义什么意思了许多可访问: n是可访问的,如果所有的m ,使得m < n也可以访问。 这当然在n = 0停止,因为没有m使m < 0并且这个陈述非常简单。

data 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并不是很好。 这就是Dominique的答案所在。我在这里写的所有这些东西都已经在标准库中完成了。 它更通用( Acc数据类型实际上是在关系上进行参数化),它允许您只需使用<-rec而不必担心Acc


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

在一个关系A只是采取两个功能A S和返回Set (我们可以称之为二元谓词):

Rel : Set → Set₁
Rel A = A → A → Set

我们可以通过更改硬编码_<_ : ℕ → ℕ → Set为某种类型A的任意关系来轻松概括Acc

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)

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

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

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

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的’:

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的函数:

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

它有助于为Decomp定义+1

_+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 > Binary2*_ ~> _,zero2*_+1 〜> _,onedecomp 〜> natToBinflatten 〜> countBits

在接受了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 ab因为div ab返回ceiling(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])

你不能直接这样做:Agda的终止检查器只考虑语法上较小的参数的递归。 但是, Agda标准库提供了一些模块,用于使用函数参数之间有充分理由的顺序来证明终止。 自然数的标准顺序就是这样的顺序,可以在这里使用。

使用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))

我在这里找到了一篇有一些解释的文章。 但可能有更好的参考。

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

你可以在这里找到整个主题: http//comments.gmane.org/gmane.comp.lang.agda/5690

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

{-# 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的简化版本go (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 ,并呼吁gofx (我们可以写go {x = fx} ... ),这是结构更小。

我们可以在calls方面重新定义⌊_/2⌋s

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

⌊ n /2⌋s返回[] ,仅当n0guarded = λ n -> n > 0 ⌊ n /2⌋s 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 ,没有任何终止问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM