Прагма REWRITE в Agda
В этой небольшой статье расскажу про прагму REWRITE, с помощью которой можно заставить компилятор считать разные определения одинаковыми, на примере сложения натуральных чисел.
Не путать с конструкцией rewrite
Определим модуль и импортируем модули для работы с равенством типов
{-# OPTIONS --rewriting #-}
module Rewriting where
import Relation.Binary.PropositionalEquality as Eq
open Eq
open Eq.≡-Reasoning
Для примера определим натуральные числа:
data ℕ : Set where
zero : ℕ
suc : ℕ → ℕ
И функцию сложения натуральных чисел
_+_ : ℕ → ℕ → ℕ
zero + b = b
(suc a) + b = suc (a + b)
Теперь можно доказать утверждение о том, что zero
это левый нейтральный элемент
l-unit : ∀ {n} → zero + n ≡ n
l-unit = refl
refl
можно воспринимать как “верно по определению”. Теперь нужно импортировать правило “переписывания” для равенства типов _≡_
:
open import Agda.Builtin.Equality.Rewrite
фактически в этом файле содержится только {-# BUILTIN REWRITE _≡_ #-}
, что “говорит” компилятору о том, что если у нас есть a ≡ b
, то можно заменять a
на b
и наоборот.
Теперь докажем, что zero
это и правый нетрайльный элемент:
r-unit : ∀ {n} → n + zero ≡ n
r-unit {n = zero} = refl
r-unit {n = suc a} = cong suc (r-unit {n = a})
{-# REWRITE r-unit #-}
а так же докажем, что если мы будем выносить suc
не из первого, а из второго аргумента за скобку, то это ничего не поменяет
+suc : ∀ {m n} → m + (suc n) ≡ suc (m + n)
+suc {zero} = refl
+suc {suc m} = cong suc (+suc {m})
{-# REWRITE +suc #-}
т.е. мы фактически доказали, что наше определение сложения эквивалентно следующему:
_+_ : ℕ → ℕ → ℕ
a + zero = a
a + (suc b) = suc (a + b)
и “сказали” компилятору с помощью прагмы rewrite, чтобы он имел это ввиду. Теперь можно очень просто доказать коммутативность сложения:
+comm : ∀ {n m} → m + n ≡ n + m
+comm {m = zero} = refl
+comm {n = a} {m = suc b} = cong suc (+comm {n = a} {m = b})
Если убрать REWRITE будет ошибка компиляции
n != n + zero of type ℕ when checking that the expression refl has type (zero + n) ≡ (n + zero)
без rewrite нам бы пришлось доказывать следующую лемму, которая теперь верна “по определению”:
m+1+n=1+m+n : ∀ {m n} → m + suc n ≡ suc m + n
m+1+n=1+m+n = refl
да и само доказательство выглядело бы сильно сложнее
+-comm` : ∀ {a b} → a + b ≡ b + a
+-comm` {zero} {b} = sym(r-unit {b}) --заменяется на refl
+-comm` {suc a} {b} = begin
suc a + b ≡⟨⟩
suc (a + b) ≡⟨ cong suc (+-comm` {a} {b}) ⟩
suc (b + a) ≡⟨⟩
suc b + a ≡⟨ m+1+n=1+m+n {b} {a} ⟩ --тоже можно заменить на refl
b + suc a ∎
Минусы и подводные камни
Данная прагма не считается “безопасной” т.е. конфликтует с опцией --safe
и соответственно такие доказательства нельзя использовать в “безопасных” модулях.
Документация объясняет это тем, что это может, например, нарушить сходимость,
т.е. в теории можно придумать пример, когда компилятор бесконечно будет перебирать разные варианты переписывания и ни к чему не придет.
UPD: Вообще не в теории, пишите {-# REWRITE +comm #-}
и наслаждаетесь бесконечной компиляцией.
Ссылки
Еще примеры можно найти тут