D8: Indukcja wzajemna


Require Import Arith.

Indukcja wzajemna

Jest jeszcze jeden rodzaj indukcji, o którym dotychczas nie mówiliśmy: indukcja wzajemna (ang. mutual induction). Bez zbędnego teoretyzowania zbadajmy sprawę na przykładzie klasyków polskiej literatury:
Smok to wysuszony zmok
Zmok to zmoczony smok
Stanisław Lem
Idea stojąca za indukcją wzajemną jest prosta: chcemy przez indukcję zdefiniować jednocześnie dwa obiekty, które mogą się nawzajem do siebie odwoływać.
W owym definiowaniu nie mamy rzecz jasna pełnej swobody — obowiązują te same kryteria co w przypadku zwykłych, "pojedynczych" definicji typów induktywnych. Wobec tego zauważyć należy, że definicja słowa "smok" podana przez Lema jest według Coqowych standardów nieakceptowalna, gdyż jeżeli w definicji smoka rozwiniemy definicję zmoka, to otrzymamy
Smok to wysuszony zmoczony smok
Widać gołym okiem, iż próba zredukowania (czyli obliczenia) obiektu smok nigdy się nie skończy. Jak już wiemy, niekończące się obliczenia w logice odpowiadają sprzeczności, a zatem ani smoki, ani zmoki w Coqowym świecie nie istnieją.
Nie znaczy to bynajmniej, że wszystkie definicje przez indukcję wzajemną są w Coqu niepoprawne, choć należy przyznać, że są dość rzadko używane. Czas jednak abyśmy ujrzeli pierwszy prawdziwy przkład indukcji wzajemnej.

Module MutInd.

Inductive even : nat -> Prop :=
| even0 : even 0
| evenS : forall n : nat, odd n -> even (S n)

with odd : nat -> Prop :=
| oddS : forall n : nat, even n -> odd (S n).

Aby zrozumieć tę definicję, zestawmy ją z naszą definicją parzystości z sekcji Induktywne predykaty.
Zdefiniowaliśmy tam predykat bycia liczbą parzystą tak:
Tym razem jednak nie definiujemy jedynie predykatu "jest liczbą parzystą". Definiujemy jednocześnie dwa predykaty: "jest liczbą parzystą" oraz "jest liczbą nieparzystą", które odwołują się do siebi nawzajm. Definicja brzmi tak:
Czy definicja taka rzeczywiście ma sens? Sprawdźmy to:
Jak widać, za pomocą naszej wzajemnie induktywnej definicji even można wygenerować wszystkie liczby parzyste (i tylko je), tak więc nowe even jest równoważne staremu even z sekcji Induktywne predykaty. Podobnie odd może wygenerować wszystkie liczby nieparzyste i tylko je.

Ćwiczenie (upewniające)

Upewnij się, że powyższy akapit nie kłamie.

Lemma even_0 : even 0.

Lemma odd_1 : odd 1.

Lemma even_2 : even 2.

Lemma even_42 : even 42.

Lemma not_odd_0 : ~ odd 0.

Lemma not_even_1 : ~ even 1.

Ćwiczenie (właściwości even i odd)

Udowodnij podstawowe właściwości even i odd.

Lemma even_SS :
  forall n : nat, even n -> even (S (S n)).

Lemma odd_SS :
  forall n : nat, odd n -> odd (S (S n)).

Lemma even_plus :
  forall n m : nat, even n -> even m -> even (n + m).

Jeśli poległeś przy ostatnim zadaniu — nie przejmuj się. Specjalnie dobrałem złośliwy przykład.
W tym momencie należy sobie zadać pytanie: jak dowodzić właściwości typów wzajemnie induktywnych? Aby udzielić odpowiedzi, spróbujmy udowodnić even_plus za pomocą indukcji po n, a potem prześledźmy, co poszło nie tak.

Lemma even_plus_failed_1 :
  forall n m : nat, even n -> even m -> even (n + m).
Proof.
  induction n; intros.
    assumption.
    cbn. constructor. inversion H; subst.
Abort.

Nasza indukcja po n zawiodła, gdyż nasza hipoteza indukcyjna ma w konkluzji even (n + m), podczas gdy nasz cel jest postaci odd (n + m). Zauważmy, że teoretycznie cel powinno dać się udowodnić, jako że mamy hipotezy even m oraz odd n, a suma liczby parzystej i nieparzystej jest nieparzysta.
Nie zrażajmy się jednak i spróbujmy indukcji po dowodzie even n.

Lemma even_plus_failed_2 :
  forall n m : nat, even n -> even m -> even (n + m).
Proof.
  induction 1; cbn; intro.
    assumption.
    constructor.
Abort.

Nasza indukcja po dowodzie hipotezy even n zawiodła, i to z kretesem, gdyż w kontekście nie mamy nawet żadnej hipotezy indukcyjnej! Co właściwie się stało?

Check even_ind.
(* ===> even_ind :
     forall P : nat -> Prop,
     P 0 -> (forall n : nat, odd n -> P (S n)) ->
       forall n : nat, even n -> P n *)


Jak widać, w naszej hipotezie "indukcyjnej" wygenerowanej przez Coqa w ogóle nie ma żadnej indukcji. Jest tam jedynie odwołanie do predykatu odd...
Zauważmy jednak, że naszym celem znów było odd (n + m), a hipotezy odd n oraz even m sprawiają, że w teorii powinno dać się ten cel udowodnić, gdyż suma liczby parzystej i nieparzystej jest nieparzysta.
Mogłoby się zdawać, że cierpimy na niedopasowanie (próba 1) lub brak (próba 2) hipotez indukcyjnych. Wydaje się też, że skoro w obydwu próbach zatrzymaliśmy się na celu odd (n + m), to pomocne mogłoby okazać się poniższe twierdzenie.

Lemma odd_even_plus_failed :
  forall n m : nat, odd n -> even m -> odd (n + m).
Proof.
  induction n; intros.
    inversion H.
    cbn. constructor. inversion H; subst.
Abort.

Niestety — nie dla psa kiełbasa, gdyż natykamy się na problemy bliźniaczo podobne do tych, które napotkaliśmy w poprzednim twierdzeniu: nasza hipoteza indukcyjna ma w konkluzji odd (n + m), podczas gdy nasz cel jest postaci even (n + m).
Próba przepchnięcia lematu za pomocą indukcji po dowodzie hipotezy odd n także nie zadziała, z tych samych powodów dla których indukcja po even n nie pozwoliła nam udowodnić even_plus. Zauważmy jednak, że cel jest udowadnialny, gdyż jako hipotezy mamy even n oraz even m, a suma dwóch liczb parzystych jest parzysta.
Wydaje się, że wpadliśmy w błędne koło i jesteśmy w matni, bez wyjścia, bez nadziei, bez krzty szans na powodzenie: w dowodzie even_plus potrzebujemy lematu odd_even_plus, ale nie możemy go udowodnić, gdyż w dowodzie odd_even_plus wymagane jest użycie lematu even_plus. Ehhh, gdybyśmy tak mogli udowodnić oba te twierdzenia na raz...
Eureka!
Zauważ, że w naszych dotychczasowych dowodach przez indukcję posługiwaliśmy się zwykłą, "pojedynczą" indukcją. Była ona wystarczająca, gdyż mieliśmy do czynienia jedynie ze zwykłymi typami induktywnymi. Tym razem jednak jest inaczej: w ostatnich trzech dowodach chcieliśmy użyć "pojedynczej" indukcji do udowodnienia czegoś na temat predykatów wzajemnie induktywnych.
Jest to ogromny zgrzyt. Do dowodzenia właściwości typów wzajemnie induktywnych powinniśmy użyć... o zgrozo, jak mogliśmy to przeoczyć, przecież to takie oczywiste... indukcji wzajemnej!
Najprostszy sposób przeprowadzenia tego dowodu wygląda tak:

Lemma even_plus :
  forall n m : nat, even n -> even m -> even (n + m)
with odd_even_plus :
  forall n m : nat, odd n -> even m -> odd (n + m).
Proof.
  assumption.
  assumption.
Fail Qed.
Restart.
  destruct n as [| n']; cbn; intros.
    assumption.
    constructor. apply odd_even_plus.
      inversion H. assumption.
      assumption.
  destruct n as [| n']; cbn; intros.
    inversion H.
    constructor. apply even_plus.
      inversion H. assumption.
      assumption.
Qed.

Co tu się właściwie stało? Pierwsze dwie linijki są takie same jak poprzednio: stwierdzamy, że będziemy dowodzić twierdzenia o podanej nazwie i postaci. Następnie mamy słowo kluczowe with, które pełni tu rolę podobną jak w definicjach przez indukcję wzajemną: podając po nim nazwę i postać twierdzenia mówimy Coqowi, że chcemy dowodzić tego twierdzenia (odd_even_plus) jednocześnie z poprzednim (even_plus).
Dotychczas po rozpoczęciu dowodu ukazywał nam się jeden cel. Tym razem, jako że dowodzimy dwóch twierdzeń jednocześnie, mamy przed sobą dwa cele. W kontekście mamy też od razu dwie hipotezy indukcyjne. Musimy na nie bardzo uważać: dotychczas hipotezy indukcyjne pojawiały się dopiero w kroku indukcyjnym i sposób ich użycia był oczywisty. Tym razem jest inaczej — jako, że mamy je od samego początku, możemy natychmiast użyć ich do "udowodnienia" naszych twierdzeń.
Niestety, takie "udowodnienie" odpowiada wywołaniu rekurencyjnemu na argumencie, który nie jest strukturalnie mniejszy (coś jak f x := f x). Fakt ten obrazuje wiadomość o błędzie, jaką Coq daje nam po tej próbie:

(* ===> Error: Cannot guess decreasing argument of fix. *)

Zaczynamy dowód od nowa, tym razem już bez oszukiwania. Musimy udowodnić każdy z naszych celów osobno, ale możemy korzystać z obydwu hipotez indukcyjnych. W obydwu celach zaczynamy od analizy przypadków, czyli rozbicia n, i rozwiązania przypadku bazowego. Rozbicie n dało nam n', które jest strukturalnie mniejsze od n, a zatem możemy bez obaw użyć naszej hipotezy indukcyjnej. Reszta jest trywialna.

Lemma even_double :
  forall n : nat, even (2 * n).
Proof.
  induction n as [| n']; cbn in *; constructor.
  rewrite <- plus_n_O in *. rewrite Nat.add_comm. cbn. constructor.
    assumption.
Qed.

End MutInd.

Reguły indukcji dla typów wzajemnych (TODO)


Module mutual.

Unset Elimination Schemes.

Inductive Smok : Type :=
| Wysuszony : Zmok -> Smok

with Zmok : Type :=
| Zmoczony : Smok -> Zmok.

Indukcja wzajemna pozwala definiować na raz wiele typów, które mogą odwoływać się do siebie nawzajem. Cytując klasyków: smok to wysuszony zmok, zmok to zmoczony smok.

Definition Smok_case_nondep_type : Type :=
  forall S : Type, (Zmok -> S) -> Smok -> S.

Definition Zmok_case_nondep_type : Type :=
  forall Z : Type, (Smok -> Z) -> Zmok -> Z.

Reguła niezależnej analizy przypadków dla Smoka wygląda banalnie: jeżeli ze Zmoka potrafimy wyprodukować S, to ze Smoka też. Dla Zmoka jest analogicznie.

Definition Smok_case_nondep
  (S : Type) (Wy : Zmok -> S) (smok : Smok) : S :=
match smok with
| Wysuszony zmok => Wy zmok
end.

Definition Zmok_case_nondep
  (Z : Type) (Zm : Smok -> Z) (zmok : Zmok) : Z :=
match zmok with
| Zmoczony smok => Zm smok
end.

Implementacja jest banalna.

Definition Smok_rec_type : Type :=
  forall S Z : Type, (Z -> S) -> (S -> Z) -> Smok -> S.

Definition Zmok_rec_type : Type :=
  forall S Z : Type, (Z -> S) -> (S -> Z) -> Zmok -> Z.

Typ rekursora jest jednak nieco bardziej zaawansowany. Żeby zdefiniować funkcję typu Smok -> S, musimy mieć nie tylko rzeczy w kształcie konstruktorów Smoka, ale także w kształcie konstruktorów Zmoka, gdyż rekurencyjna struktura obu typów jest ze sobą nierozerwalnie związana.

Fixpoint Smok_rec
  (S Z : Type) (Wy : Z -> S) (Zm : S -> Z) (smok : Smok) : S :=
match smok with
| Wysuszony zmok => Wy (Zmok_rec S Z Wy Zm zmok)
end

with Zmok_rec
  (S Z : Type) (Wy : Z -> S) (Zm : S -> Z) (zmok : Zmok) : Z :=
match zmok with
| Zmoczony smok => Zm (Smok_rec S Z Wy Zm smok)
end.

Implementacja wymaga rekursji wzajemnej, ale poza nie jest jakoś wybitnie groźna.

Definition Smok_ind_type : Type :=
  forall (S : Smok -> Type) (Z : Zmok -> Type)
    (Wy : forall zmok : Zmok, Z zmok -> S (Wysuszony zmok))
    (Zm : forall smok : Smok, S smok -> Z (Zmoczony smok)),
      forall smok : Smok, S smok.

Definition Zmok_ind_type : Type :=
  forall (S : Smok -> Type) (Z : Zmok -> Type)
    (Wy : forall zmok : Zmok, Z zmok -> S (Wysuszony zmok))
    (Zm : forall smok : Smok, S smok -> Z (Zmoczony smok)),
      forall zmok : Zmok, Z zmok.

Fixpoint Smok_ind
  (S : Smok -> Type) (Z : Zmok -> Type)
  (Wy : forall zmok : Zmok, Z zmok -> S (Wysuszony zmok))
  (Zm : forall smok : Smok, S smok -> Z (Zmoczony smok))
  (smok : Smok) : S smok :=
match smok with
| Wysuszony zmok => Wy zmok (Zmok_ind S Z Wy Zm zmok)
end

with Zmok_ind
  (S : Smok -> Type) (Z : Zmok -> Type)
  (Wy : forall zmok : Zmok, Z zmok -> S (Wysuszony zmok))
  (Zm : forall smok : Smok, S smok -> Z (Zmoczony smok))
  (zmok : Zmok) : Z zmok :=
match zmok with
| Zmoczony smok => Zm smok (Smok_ind S Z Wy Zm smok)
end.

Mając rekursor, napisanie typu reguły indukcji jest banalne, podobnie jak jego implementacja.

Fixpoint even (n : nat) : bool :=
match n with
| 0 => true
| S n' => odd n'
end

with odd (n : nat) : bool :=
match n with
| 0 => false
| S n' => even n'
end.

End mutual.