Проверка завершения Agda не удалась для упражнения reverse-++-distrib.

Вопрос или проблема

Проверка завершения Agda не удалась для упражнения reverse-++-distrib.

Я выполнял задачу по коду в Agda из PLFA, чтобы реализовать доказательство того, что “обратный порядок одного списка, добавленного к другому, равен обратному порядку второго списка, добавленного к обратному порядку первого”.

reverse-++-distrib : ∀ {A : Set} → (xs ys : List A) → reverse (xs ++ ys) ≡ reverse ys ++ reverse xs

Я смог сделать первые два случая индукции без проблем:

reverse-++-distrib {A} [] ys

reverse-++-distrib xs []

но третья стадия выдает ошибку “Проверка завершения не удалась”, что, я полагаю, означает, что индуктивный шаг выполнен неправильно.

reverse-++-distrib {A} (x :: xs) ys =
  begin
    reverse ((x :: xs) ++ ys)
  ≡⟨⟩
    reverse (x :: (xs ++ ys))
  ≡⟨⟩
    reverse (xs ++ ys) ++ [ x ]
  ≡⟨ cong (_++ [ x ]) (reverse-++-distrib xs ys) ⟩
    (reverse ys ++ reverse xs) ++ [ x ]
  ≡⟨ ++-assoc (reverse ys) (reverse xs) [ x ] ⟩
    reverse ys ++ (reverse xs ++ [ x ])
  ≡⟨ cong ((reverse ys) ++_) (cong ((reverse xs) ++_) (sym (reverse-++-fixed-point {A} x))) ⟩
    (reverse ys) ++ (reverse xs ++ (reverse [ x ]))
  **≡⟨ cong ((reverse ys) ++_) (sym (reverse-++-distrib {A} (x :: []) xs)) ⟩**
    (reverse ys) ++ (reverse ([ x ] ++ xs))
  ≡⟨⟩
    (reverse ys) ++ (reverse (x :: ([] ++ xs)))
  ≡⟨ cong ((reverse ys) ++_) (cong reverse (cong (x ::_) (++-identityˡ xs))) ⟩
    (reverse ys) ++ (reverse (x :: xs))
  ∎

Проблемный вызов, по-видимому, это:

reverse-++-distrib {A} (x :: []) xs

Для меня это выглядит нормально, потому что длина первого аргумента равна 1, что меньше или равно длине x :: xs. И она равна, только если xs – пустой список, в этом случае он должен перейти ко второму сопоставлению с образцом. Так почему же у Agda возникают проблемы с этой определением?

Для меня это выглядит нормально, потому что длина первого аргумента равна 1, что меньше или равно длине x :: xs. И она равна, только если xs – пустой список, в этом случае он должен перейти ко второму сопоставлению с образцом. Так почему же у Agda возникают проблемы с этой определением?

Agda совсем не способна угадать такой аргумент на пустом месте. Проверка завершения только гарантирует, что аргументы функции становятся структурно меньше с каждым вызовом (при условии подходящего лексикографического порядка), но ваша функция не удовлетворяет этому требованию, потому что x ∷ [] не меньше структурно, чем x ∷ xs (как вы заметили, они могут быть равны).

Вы можете считать это ограничительным, но это часто показывает более естественный способ структурировать программы и доказательства: например, здесь вы можете обойти это, превратив reverse-++-distrib [ x ] xs в отдельную лемму reverse-∷-distrib x xs : reverse (x ∷ xs) ≡ reverse xs ++ [ x ].

П.С.: ваш последний шаг — это просто refl, так как [] ++ xs = xs по определению.

Ответ или решение

Проблема, с которой вы столкнулись в Agda, связана с тем, что проверка окончания (termination checking) не проходит из-за того, что аргумент функции не уменьшается структурно при рекурсивном вызове. Это происходит в вашем случае, когда вы пытаетесь вызвать reverse-++-distrib {A} (x :: []) xs, поскольку x :: [] не является строго меньшим, чем x :: xs.

Agda требует, чтобы для обеспечения корректности завершения рекурсивных вызовов аргументы функции должны быть «структурно меньшими» в каждом шаге рекурсии. В вашем случае ситуация такова, что длина x :: [] может быть равна длине x :: xs, если xs – пустой список, что не соответствует требованиям завершения.

Чтобы обойти эту проблему, вам следует рассмотреть возможность выделения отдельной леммы, которая будет решать эту частную задачу. Например, вы можете создать лему:

reverse-∷-distrib : ∀ {A : Set} → (x : A) → (xs : List A) → reverse (x :: xs) ≡ reverse xs ++ [ x ]

Затем вы можете использовать эту лемму в вашем доказательстве. Вот как можно перестроить ваш код с добавлением этой леммы:

reverse-∷-distrib : ∀ {A : Set} → (x : A) → (xs : List A) → reverse (x :: xs) ≡ reverse xs ++ [ x ]
reverse-∷-distrib x xs = -- Ваша реализация леммы

reverse-++-distrib : ∀ {A : Set} → (xs ys : List A) → reverse (xs ++ ys) ≡ reverse ys ++ reverse xs
reverse-++-distrib {A} [] ys = refl
reverse-++-distrib {A} xs [] = -- Ваша реализация для второго случая
reverse-++-distrib {A} (x :: xs) ys =
  begin
    reverse ((x :: xs) ++ ys)
  ≡⟨⟩
    reverse (x :: (xs ++ ys))
  ≡⟨⟩
    reverse (xs ++ ys) ++ [ x ]
  ≡⟨ cong (_++ [ x ]) (reverse-++-distrib xs ys) ⟩
    (reverse ys ++ reverse xs) ++ [ x ]
  ≡⟨ ++-assoc (reverse ys) (reverse xs) [ x ] ⟩
    reverse ys ++ (reverse xs ++ [ x ])
  ≡⟨ cong ((reverse ys) ++_) (cong ((reverse xs) ++_) (sym (reverse-++-fixed-point {A} x))) ⟩
    (reverse ys) ++ (reverse xs ++ (reverse [ x ]))
  ≡⟨ cong ((reverse ys) ++_) (sym (reverse-++-distrib {A} (x :: []) xs)) ⟩
    (reverse ys) ++ (reverse ([ x ] ++ xs))
  ≡⟨⟩
    (reverse ys) ++ (reverse (x :: ([] ++ xs)))
  ≡⟨ cong ((reverse ys) ++_) (cong reverse (cong (x ::_) (++-identityˡ xs))) ⟩
    (reverse ys) ++ (reverse (x :: xs))
  ∎

Теперь использование reverse-∷-distrib вместо вызова reverse-++-distrib {A} (x :: []) xs решит проблему с проверкой завершения, так как вы избежите ситуации, когда длины списков становятся равными.

Важно отметить, что последний шаг вашего доказательства идентичен refl, поскольку по определению [] ++ xs = xs.

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

Оцените материал
Добавить комментарий

Капча загружается...