D2i: Rekursja zagnieżdżona [TODO]


Require Import List.
Import ListNotations.

Require Import Arith Lia.

From Typonomikon Require Import D5.
From Typonomikon Require Export D2h.

Rekursja zagnieżdżona

Jakież to diabelstwo może być tak diabelskie, by przeciwstawić się metodzie induktywnej dziedziny oraz komendzie Function? Ano ano, rekursja zagnieżdżona - wywołanie rekurencyjne jest zagnieżdżone, jeżeli jego argumentem jest wynik innego wywołania rekurencyjnego.

Module McCarthy.

Fail Fixpoint f (n : nat) : nat :=
  if 100 <? n then n - 10 else f (f (n + 11)).

Fail Function f (n : nat) {measure id n} : nat :=
  if 100 <? n then n - 10 else f (f (n + 11)).

Ta funkcja jest podobna zupełnie do niczego, co dotychczas widzieliśmy. Działa ona następująco:
Taka rekursja jest oczywiście nielegalna: n + 11 nie jest strukturalnym podtermem n, gdyż jest od niego większe, zaś f (n + 11) w ogóle nie wiadomo a priori, jak się ma do n. Nie dziwota więc, że Coq odrzuca powyższą definicję.
Być może wobec tego taka "funkcja" w ogóle nie jest funkcją, a definicja jest wadliwa? Otóż nie tym razem. Okazuje się bowiem, że istnieje funkcja zachowująca się zgodnie z zawartym w definicji równaniem. Żebyśmy mogli w to uwierzyć, zastanówmy się, ile wynosi f 100.
f 100 = f (f 111) = f 101 = 101 - 10 = 91 - poszło gładko. A co z 99? Mamy f 99 = f (f 110) = f 100 = 91 - znowu 91, czyżby spiseg? Dalej: f 98 = f (f 109) = f 99 = 91 - tak, to na pewno spiseg. Teraz możemy zwerbalizować nasze domysły: jeżeli n <= 100, to f n = 91. Jak widać, nieprzypadkowo funkcja ta bywa też nazywana "funkcją 91 McCarthy'ego".
Czy da się tę funkcję zaimplementować w Coqu? Jeszcze jak!

Definition f_troll (n : nat) : nat :=
  if n <=? 100 then 91 else n - 10.

Ehhh... nie tego się spodziewałeś, prawda? f_troll jest wprawdzie implementacją opisanej powyżej nieformalnie funkcji f, ale definicja opiera się na tym, że z góry wiemy, jaki jest wynik f dla dowolnego argumentu. Nie trzeba chyba tłumaczyć, że dla żadnej ciekawej funkcji nie będziemy posiadać takiej wiedzy (a sama funkcja McCarthy'ego nie jest ciekawa, bo jest sztuczna, ot co!).
Czy więc da się zaimplementować f bezpośrednio, tzn. w sposób dokładnie oddający definicję nieformalną? Otóż tak, da się i to w sumie niewielkim kosztem: wystarczy jedynie nieco zmodyfikować naszą metodę induktywnej dziedziny. Zanim jednak to zrobimy, zobaczmy, dlaczego nie obejdzie się bez modyfikacji.

Fail Inductive fD : nat -> Type :=
| fD_gt100 : forall n : nat, 100 < n -> fD n
| fD_le100 :
    forall n : nat, n <= 100 ->
      fD (n + 11) -> fD (f (n + 11)) -> fD n.

(* ===> The command has indeed failed with message:
        The reference f was not found in the current environment. *)


A oto i źródło całego problemu. Jeżeli n <= 100, to chcemy zrobić dwa wywołania rekurencyjne: jedno na n + 11, a drugie na f (n + 11). Wobec tego należenie tych dwóch argumentów do dziedziny jest warunkiem należenia n do dziedziny i stąd postać całego konstruktora.
Niestety, definicja jest zła - f (n + 11) nie jest poprawnym termem, gdyż f nie jest jeszcze zdefiniowane. Mamy więc błędne koło: żeby zdefiniować f, musimy zdefiniować predykat dziedziny fD, ale żeby zdefiniować fD, musimy zdefiniować f.
Jak wyrwać się z tego błędnego koła? Ratunek przychodzi ze strony być może nieoczekiwanej, ale za to już bardzo dobrze przez nas poznanej, a jest nim induktywna definicja wykresu. Tak tak - w definicji fD możemy (a nawet musimy) zastąpić wystąpienia f przez wystąpienia wykresu f.
Hej ho, po przykład by się szło.

Inductive fG : nat -> nat -> Prop :=
| fG_gt100 :
    forall n : nat, 100 < n -> fG n (n - 10)
| fG_le100 :
    forall n r1 r2 : nat,
      n <= 100 -> fG (n + 11) r1 -> fG r1 r2 -> fG n r2.

Tak wygląda wykres funkcji f. Wywołanie rekurencyjne f (f (n + 11) możemy zareprezentować jako dwa argumenty, mianowicie fG (n + 11) r1 i fG r1 r2. Dosłownie odpowiada to wywołaniu rekurencyjnemu w stylu let r1 := f (n + 11) in let r2 := f r1 in r2.

Lemma fG_det :
  forall n r1 r2 : nat,
    fG n r1 -> fG n r2 -> r1 = r2.
Proof.
  intros until 1. revert r2.
  induction H; intros r Hr.
    inversion Hr; subst.
      reflexivity.
      abstract lia.
    inversion Hr; subst.
      abstract lia.
      assert (r1 = r0) by apply (IHfG1 _ H3); subst.
        apply (IHfG2 _ H4).
Defined.

Po zdefiniowaniu wykresu dowodzimy, podobnie łatwo jak poprzednio, że jest on relacją deterministyczną.

Inductive fD : nat -> Type :=
| fD_gt100 :
    forall n : nat, 100 < n -> fD n
| fD_le100 :
    forall n r : nat, n <= 100 ->
      fG (n + 11) r -> fD (n + 11) -> fD r -> fD n.

A tak wygląda definicja predykatu dziedziny. Zamiast fD (f (n + 11)) mamy fD r, gdyż r na mocy argumentu fG (n + 11) r reprezentuje wynik wywołania rekurencyjnego f (n + 11).

Fixpoint f' {n : nat} (d : fD n) : nat :=
match d with
| fD_gt100 _ _ => n - 10
| fD_le100 _ _ _ _ _ d2 => f' d2
end.

Definicja funkcji pomocniczej f' może być nieco zaskakująca: gdzie podziało się zagnieżdżone wywołanie rekurencyjne? Nie możemy jednak dać się zmylić przeciwnikowi. Ostatnią klauzulę dopasowania do wzorca możemy zapisać jako | fD_le100 n r H g d1 d2 => f' d2. Widzimy, że d2 jest typu fD r, ale g : fG (n + 11) r, więc możemy myśleć, że r to tak naprawdę f (n + 11), a zatem d2 tak naprawdę jest typu fD (f (n + 11)). Jeżeli dodatkowo napiszemy wprost domyślny argument f', to wywołanie rekurencyjne miałoby postać @f' (@f' (n + 11) d1) d2, a więc wszystko się zgadza. Żeby jednak nie rzucać słów na wiatr, udowodnijmy to.

Lemma f'_correct :
  forall (n : nat) (d : fD n), fG n (f' d).
Proof.
  induction d; cbn.
    constructor. assumption.
    econstructor 2.
      assumption.
      exact IHd1.
      assert (r = f' d1).
        apply fG_det with (n + 11); assumption.
        subst. assumption.
Defined.

Dowód twierdzenia o poprawności jest tylko odrobinkę trudniejszy niż ostatnio, gdyż w przypadku wystąpienia w kontekście dwóch hipotez o typie fG (n + 11) _ musimy użyć twierdzenia o determinizmie wykresu.

Lemma f'_complete :
  forall (n r : nat) (d : fD n),
    fG n r -> f' d = r.
Proof.
  intros. apply fG_det with n.
    apply f'_correct.
    assumption.
Defined.

Dowód twierdzenia o pełności pozostaje bez zmian.

Lemma fG_le100_spec :
  forall n r : nat,
    fG n r -> n <= 100 -> r = 91.
Proof.
  induction 1; intro.
    abstract lia.
    inversion H0; subst.
      inversion H1; subst.
        assert (n = 100) by abstract lia. subst. reflexivity.
        abstract lia.
      abstract lia.
Defined.

Lemma f'_le100 :
  forall (n : nat) (d : fD n),
    n <= 100 -> f' d = 91.
Proof.
  intros. apply fG_le100_spec with n.
    apply f'_correct.
    assumption.
Defined.

Lemma f'_ge100 :
  forall (n : nat) (d : fD n),
    100 < n -> f' d = n - 10.
Proof.
  destruct d; cbn; abstract lia.
Defined.

Teraz następuje mały twist. Udowodnienie, że każdy argument spełnia fD będzie piekielnie trudne i będziemy w związku z tym potrzebować charakteryzacji funkcji f'. Zaczynamy więc od udowodnienia, że dla n <= 100 wynikiem jest 91. Najpierw robimy to na wykresie, bo tak jest łatwiej, a potem transferujemy wynik na funkcję. Charakteryzację dla 100 < n dostajemy wprost z definicji.

Lemma fD_all :
  forall n : nat, fD n.
Proof.
  apply (well_founded_ind _ (fun n m => 101 - n < 101 - m)).
    apply wf_inverse_image. apply wf_lt.
    intros n IH. destruct (le_lt_dec n 100).
      assert (d : fD (n + 11)) by (apply IH; lia).
        apply fD_le100 with (f' d).
          assumption.
          apply f'_correct.
          assumption.
          apply IH. inversion d; subst.
            rewrite f'_ge100.
              abstract lia.
              assumption.
            rewrite f'_le100; abstract lia.
      constructor. assumption.
Defined.

Dowód jest przez indukcję dobrze ufundowaną po n, a relacja dobrze ufundowana, której używamy, to fun n m : nat => 101 - n < 101 - m. Dlaczego akurat taka? Przypomnijmy sobie, jak dokładnie oblicza się funkcja f, np. dla 95:
f 95 = f (f 106) = f 96 = f (f 107) = f 97 = f (f 108) = f 98 = f (f 109) = f 99 = f (f 110) = f 100 = f (f 111) = f 101 = 91.
Jak więc widać, im dalej w las, tym bardziej zbliżamy się do magicznej liczby 101. Wyrażenie 101 - n mówi nam, jak blisko przekroczenia 101 jesteśmy, a więc 101 - n < 101 - m oznacza, że każde wywołanie rekurencyjne musi być bliżej 101 niż poprzednie wywołanie. Oczywiście zamiast 101 może być dowolna większa liczba - jeżeli zbliżamy się do 101, to zbliżamy się także do 1234567890.
Dowód dobrego ufundowania jest banalny, ale tylko pod warunkiem, że zrobiłeś wcześniej odpowiednie ćwiczenie. Jeszcze jedna uwaga: jak wymyślić relację dobrze ufundowaną, jeżeli jest nam potrzebna przy dowodzie takim jak ten? Mógłbym ci tutaj naopowiadać frazesów o... w sumie nie wiem o czym, ale prawda jest taka, że nie wiem, jak się je wymyśla. Tej powyższej wcale nie wymyśliłem sam - znalazłem ją w świerszczyku dla bystrzaków.
Dobra, teraz właściwa część dowodu. Zaczynamy od analizy przypadków. Drugi przypadek, gdy 100 < n, jest bardzo łatwy. W pierwszym zaś przypadku z hipotezy indukcyjnej dostajemy fD (n + 11), tzn. n + 11 należy do dziedziny. Skoro tak, to używamy konstruktora fD_le100, a jako r (czyli wynik wywołania rekurencyjnego) dajemy mu f' d.
Dwa podcele zachodzą na mocy założenia, a jedna wynika z twierdzenia o poprawności. Pozostaje nam zatem pokazać, że f' d także należy do dziedziny. W tym celu po raz kolejny używamy hipotezy indukcyjnej. Na zakończenie robimy analizę przypadków po d, używamy charakteryzacji f' do uproszczenia celu i kończymy rozumowaniami arytmetycznymi.

Definition f (n : nat) : nat := f' (fD_all n).

(* Compute f 110. *)

Teraz możemy zdefiniować oryginalne f. Niestety, funkcja f się nie oblicza i nie wiem nawet dlaczego.


Lemma f_correct :
  forall n : nat, fG n (f n).
Proof.
  intros. apply f'_correct.
Qed.

Lemma f_complete :
  forall n r : nat,
    fG n r -> f n = r.
Proof.
  intros. apply f'_complete. assumption.
Qed.

Lemma f_91 :
  forall (n : nat),
    n <= 100 -> f n = 91.
Proof.
  intros. apply f'_le100. assumption.
Qed.

Twierdzenia o poprawności i pełności oraz charakteryzacja dla f wynikają za darmo z odpowiednich twierdzeń dla f'.

Lemma f_ind :
  forall
    (P : nat -> nat -> Prop)
    (H_gt100 : forall n : nat, 100 < n -> P n (n - 10))
    (H_le100 :
      forall n : nat, n <= 100 ->
        P (n + 11) (f (n + 11)) -> P (f (n + 11)) (f (f (n + 11))) ->
          P n (f (f (n + 11)))),
    forall n : nat, P n (f n).
Proof.
  intros. apply fG_ind.
    assumption.
    intros. apply f_complete in H0. apply f_complete in H2.
      subst. apply H_le100; assumption.
    apply f_correct.
Defined.

Reguły indukcji wykresowej dowodzimy tak samo jak poprzednio, czyli za pomocą twierdzeń o pełności i poprawności.

Lemma f_eq :
  forall n : nat,
    f n = if 100 <? n then n - 10 else f (f (n + 11)).
Proof.
  intros. apply fG_det with n.
    apply f_correct.
    unfold Nat.ltb. destruct (Nat.leb_spec0 101 n).
      constructor. assumption.
      econstructor.
        lia.
        apply f_correct.
        apply f_correct.
Qed.

Na koniec również mały twist, gdyż równanie rekurencyjne najprościej jest udowodnić za pomocą właściwości wykresu funkcji f - jeśli nie wierzysz, to sprawdź (ale będzie to bardzo bolesne sprawdzenie).
Podsumowując: zarówno oryginalna metoda induktywnej dziedziny jak i komenda Function nie radzą sobie z zagnieżdżonymi wywołaniami rekurencyjmi, czyli takimi, w których argumentem jest wynik innego wywołania rekurencyjnego. Możemy jednak poradzić sobie z tym problemem za pomocą ulepszonej metody induktywnej dziedziny, w której funkcję w definicji predykatu dziedziny reprezentujemy za pomocą jej induktywnie zdefiniowanego wykresu.

Ćwiczenie

Przyjrzyjmy się poniższej fikuśnej definicji funkcji:

Fail Fixpoint g (n : nat) : nat :=
match n with
| 0 => 0
| S n => g (g n)
end.

Wytłumacz, dlaczego Coq nie akceptuje tej definicji. Następnie wymyśl twierdzenie charakteryzujące tę funkcję, a na koniec zdefiniuj ją za pomocą metody zaprezentowanej w tym podrozdziale.


End McCarthy.