Прагма 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 #-} и наслаждаетесь бесконечной компиляцией.
Ссылки
Еще примеры можно найти тут