Подтвердить что ты не робот

Помощь Агда

Предположим, что мы определим функцию

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

Агда будет рисовать f в лососе, потому что он не может определить, меньше ли n/2, чем n. Я не знаю, как сказать, что Агда прекратил проверку. Я вижу, что в стандартной библиотеке у них есть разделение на две части на 2 и доказательство того, что n/2 < п. Тем не менее, я до сих пор не понимаю, как заставить средство проверки терминалов понять, что рекурсия выполнена на меньшей подзадаче.

4b9b3361

Ответ 1

Контроллер завершения сеанса Agda проверяет только структурную рекурсию (т.е. вызовы, которые происходят на структурно меньших аргументах), и нет способа установить, что определенное отношение (например, _<_) подразумевает, что один из аргументов структурно меньше.


Отступление: Аналогичная проблема возникает при проверке положительности. Рассмотрим стандартный тип данных фиксированной точки:

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

Агда отвергает это, потому что 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 < 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 напрямую не очень приятно. И это, где приходит ответ Доминика. Все это, что я написал здесь, уже было сделано в стандартной библиотеке. Он более общий (тип данных acc на самом деле параметризирован над отношением), и он позволяет вам просто использовать <-rec, не беспокоясь о acc.


Взяв более пристальный взгляд, мы на самом деле довольно близки к общему решению. Посмотрим, что получим, когда мы параметризируем отношение. Для простоты я не имею дело с полиморфизмом Вселенной.

Отношение на A - это просто функция, принимающая два A и возвращающая Set (мы можем назвать бинарный предикат):

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

Мы можем легко обобщить acc, изменив hardcoded _<_ : ℕ → ℕ → 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)

Ответ 2

Я хотел бы предложить несколько иной ответ, чем приведенный выше. В частности, я хочу предположить, что вместо того, чтобы каким-то образом убедить проверку терминалов, что на самом деле нет, эта рекурсия совершенно прекрасна, мы должны вместо этого попытаться подтвердить обоснованность, чтобы рекурсия была явно прекрасной в силу будучи структурным.

Идея здесь в том, что проблема заключается в невозможности увидеть, что n / 2 является как-то "частью" n. Структурная рекурсия хочет разбить вещь на ее непосредственные части, но способ, которым n / 2 является "частью" n, заключается в том, что мы отбрасываем все остальные suc. Но это не очевидно, сколько из них нужно сбросить, мы должны осмотреться и попытаться разобраться. Что было бы неплохо, если бы у нас был тип, у которого были конструкторы для "multiple" 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, либо вычитаем один, и разделим на 2. Каждое натуральное число, большее 0, может быть разложено однозначно таким образом. Если он четный, разделите на два и продолжайте, если он нечетный, вычтите один и разделите на два и продолжайте.

Итак, теперь мы можем точно видеть, как должен выглядеть наш тип данных. Нам нужен тип с конструктором, который означает "в два раза больше suc", а другой означает "в два раза больше suc плюс один", а также, конечно, конструктор, который означает "zero 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 для Decomp s:

_+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)

Учитывая a 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 бит и добавить один". Таким образом, приведенный выше код действительно показывает, что двоичные числа структурно рекурсивны, делятся на 2, что они должны быть! Мне кажется, это намного легче понять, но я не хочу менять то, что я написал, поэтому мы могли бы вместо этого переименовать здесь: Decomp ~ > Binary, 2*_ ~ > _,zero, 2*_+1 ~ > _,one, Decomp ~ > natToBin, flatten ~ > countBits.

Ответ 3

После принятия ответа Витуса я обнаружил другой способ достижения цели, заключающейся в том, что функция заканчивается в Agda, а именно, используя "размерные типы". Я предоставляю свой ответ здесь, потому что это кажется приемлемым, а также для критики любых слабых сторон этого ответа.

Описанные типы описаны: http://arxiv.org/pdf/1012.4896.pdf

Они реализованы в Agda, а не только MiniAgda; см. здесь: http://www2.tcs.ifi.lmu.de/~abel/talkAIM2008Sendai.pdf.

Идея состоит в том, чтобы увеличить тип данных с размером, который позволяет typechecker более легко доказать завершение. Размер определяется в стандартной библиотеке.

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

Теперь мы можем определить деление по алгоритму Евклида:

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])

Ответ 4

Вы не можете сделать это напрямую: проверка окончания сеанса Agda учитывает только рекурсию ok на синтаксически меньших аргументах. Тем не менее, стандартная библиотека 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))

Я нашел статью с некоторым объяснением здесь. Но там могут быть лучшие ссылки.

Ответ 5

Подобный вопрос появился в рассылке Agda несколько недель назад, и консенсус, казалось, заключался в том, чтобы вставить элемент Data.Nat в Data.Bin, а затем использовать структурную рекурсию в этом представлении, которая хорошо подходит для задания под рукой.

Здесь вы можете найти целую цепочку: http://comments.gmane.org/gmane.comp.lang.agda/5690

Ответ 6

Вы можете избежать использования обоснованной рекурсии. Скажем, вам нужна функция, которая применяет ⌊_/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⌉) 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 и вызовите 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, без каких-либо проблем с завершением.