Induction.Intro
(*
CoMoEx - Induction in Coq in order to improve Pen&Paper-Proofs.
Copyright (C) 2024 Max Ole Elliger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*)
CoMoEx - Induction in Coq in order to improve Pen&Paper-Proofs.
Copyright (C) 2024 Max Ole Elliger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*)
Induktive Strukturen
Induktive Strukturen "im Alltag"
Induktive Strukturen finden sich überall in der Welt. Beispiel: Ein Lego-Schloss besteht aus mehreren einzelnen Steinen, es ist also zusammengesetzt. Wahrscheinlich wundert man sich jetzt, wo die typische Rekursion ist. Antwort: Nicht vorhanden. Wichtig ist das Wort zusammengesetzt: Induktiv meint genau das hier. Ein Lego-Schloss ist eine zusammengesetzte Struktur, zusammengesetzt aus mehreren Steinen. Das, was man wahrscheinlich typischerweise unter Rekursion versteht, kommt dadurch zustande, dass man mehrere Lego-Schlösser zu einem größeren Lego-Schloss zusammensetzt (wie auch immer das genau funktionieren soll).
Was passiert hier?
Schauen wir uns noch kurz an, wie man Schlösser zusammensetzen kann:
- Inductive ist das Schlüsselwort für zusammengesetzte Strukturen.
- castle ist der Typ unserer Lego-Schlösser.
- sets_of_stones ist eine Menge an Mengen an Steinen. Nicht weiter drüber nachdenken, das dient hier gerade nur der Darstellung.
- castle_of_stones sagt uns, wie man ein castle bauen kann: Man braucht dafür lediglich eine Menge an Steinen some_set.
Reset castle.
Inductive castle {sets_of_stones : Set} :=
| castle_of_stones (some_set : sets_of_stones) : castle
| castle_of_castles (castle_1 castle_2 : castle) : castle.
Der neue sogenannte Konstruktor castle_of_castles gibt also eine zweite
Möglichkeit an, ein castle zu bauen: Man nimmt zwei bereits bekannte
castles und baut daraus ein neues castle.
Natürliche Zahlen
Solche Beschreibungen lassen sich natürlich auch verbalisieren:
Man könnte das auch noch lascher formulieren:
Diese Formulierung unterstützt nochmal mehr, dass wir eine Menge an Dingen
gebaut haben, die wir NAT nennen, oder eben die Menge der natürlichen
Zahlen. Etwas machen können wir damit bisher nicht. Dazu später mehr.
Wir brauchen strings, um Atome repräsentieren zu können.
- zero ist eine natürliche Zahl NAT.
- Falls n bereits eine natürliche Zahl NAT ist, dann ist auch succ(n) eine natürliche Zahl NAT.
Syntax von Aussagenlogik
Hier mal ein Beispiel:
Rekursion
Mittels Rekursion kann man nun Funktionen auf solchen induktiven Strukturen definieren. Rekursion meint hier lediglich, dass man für jeden Konstruktor sagen muss, wie sich die Funktion verhalten soll. Dabei kann sich die Funktion selbst aufrufen, wenn der Konstruktor Argumente desselben induktiven Typs hat.Addition auf NAT
Hier ein Beispiel auf unseren natürlichen Zahlen NAT:Fixpoint add (n1 n2 : NAT) : NAT :=
match n1 with
| zero => n2
| succ (n1') => succ (add (n1') n2)
end.
Was passiert hier?
- Fixpoint ist das Schlüsselwort für rekursive Funktionen.
- add ist der Name unserer Funktion.
- n1 und n2 sind die beiden Argumente der Funktion, beide vom Typ NAT.
- Die Funktion gibt eine natürliche Zahl NAT zurück.
- Durch match wird eine Fallunterscheidung auf n1 gemacht.
- Falls n1 = zero, gibt die Funktion n2 zurück, weil 0 + n2 = n2.
- Falls n1 = succ (n1'), gibt die Funktion succ (add (n1') n2) zurück, weil (n1 +1) + n2 = 1 + (n1 + n2).
- end beendet die Fallunterscheidung.
Check auf zero
Solche Funktionen müssen aber keine wirkliche Rekursion verwenden, wie das folgende Beispiel zeigt:
Die Konsole sagt genau das:
Not a truly recursive fixpoint.
In solchen Fällen kann man dann auch das Schlüsselwort Definition
verwenden:
Fixpoint atom_occurs (f : formula) (a : string) : Prop :=
match f with
| top => False
| bottom => False
| atom (b) => b = a
| negation (f1) => atom_occurs (f1) (a)
| conjunction (f1) (f2) => (atom_occurs (f1) (a)) \/ (atom_occurs (f2) (a))
| disjunction (f1) (f2) => (atom_occurs (f1) (a)) \/ (atom_occurs (f2) (a))
| implication (f1) (f2) => (atom_occurs (f1) (a)) \/ (atom_occurs (f2) (a))
| biimplication (f1) (f2) => (atom_occurs (f1) (a)) \/ (atom_occurs (f2) (a))
end.
Da formula mehr Konstruktoren hat, müssen wir die rekursive Funktion
atom_occurs auch für mehr Fälle definieren. Aber das Prinzip ist das
gleiche geblieben.
Semantik von Aussagenlogik
Schauen wir uns doch einmal mit Blick auf sinnvolle Theoreme an, wie sich die typische Semantik auf Aussagenlogik definieren lässt. Dazu brauchen wir zunächst den Begriff einer Wahrheitsbelegung:
Eine Wahrheitsbelegung ist also eine Funktion, die jedem Atom einen
Wahrheitswert true oder false zuordnet. Damit lässt sich die Semantik nun
wie folgt (rekursiv) definieren:
Fixpoint satisfaction (k : wb) (f : formula) : Prop :=
match f with
| top => True
| bottom => False
| atom (a) => k (a) = true
| negation (f1) => ~ satisfaction (k) (f1)
| conjunction (f1) (f2) => satisfaction (k) (f1) /\ satisfaction (k) (f2)
| disjunction (f1) (f2) => satisfaction (k) (f1) \/ satisfaction (k) (f2)
| implication (f1) (f2) => satisfaction (k) (f1) -> satisfaction (k) (f2)
| biimplication (f1) (f2) => satisfaction (k) (f1) <-> satisfaction (k) (f2)
end.
Eine Bemerkung zur Reduktion von Verwirrung: Wir verwenden hier Prop (und
nicht bool), da wir uns in diesem Kontext Prop als das Universum unserer
"Alltagslogik" vorstellen können, während z.B. bool irgendetwas ist, was
"wir" uns selbst aufgebaut haben. Achtung: In GLoIn wird Prop meist
verwendet, um die Struktur von Fitch-Beweisen nachzubilden.
Einschränkung der möglichen Formeln
... auf bottom, atom und disjunction
Mittels des folgenden Prädikats ist es z.B. möglich, unsere Menge an Formeln einzuschänken:Fixpoint only_bottom_atom_disjunction (f : formula) : Prop :=
match f with
| top => False
| bottom => True
| atom (a) => True
| negation (f1) => False
| conjunction (f1) (f2) => False
| disjunction (f1) (f2) =>
only_bottom_atom_disjunction (f1) /\
only_bottom_atom_disjunction (f2)
| implication (f1) (f2) => False
| biimplication (f1) (f2) => False
end.
(Strukturelle) Induktion
Induktion ist nun das korrespondierende Beweisprinzip für rekursive Funktionen. Mit Rekursion definiert man eine Funktion, indem man für jeden Konstruktor eine Definitionsvorschrift angibt. Mit Induktion gibt man einen Beweis für jeden Konstruktor an, um eine Aussage für alle Elemente der induktiven Struktur zu beweisen. Schauen wir uns das direkt praktisch an:Assoziativität von add
Wir haben jetzt das Theorem niedergeschrieben, das wir beweisen wollen.
Wir wollen also zeigen, dass die Addition add, die wir oben definiert
haben, assoziativ ist, also dass n1 + (n2 + n3) = (n1 + n2) + n3 für alle
n1,n2 und n3 gilt. Mittels Proof leiten wir nun den Beweis ein.
Proof.
Mit intros n1 leiten wir den Beweis mit den typischen Worten "Sei n1
eine natürliche Zahl NAT." ein.
intros n1.
Kommen wir nun zum spannenden Teil: Induktion über (die natürliche Zahl)
n1. Durch das Pattern [|n1' IH] benennen wir die "kleineren" Argumente:
induction n1 as [|n1' IH].
-
-
Fall 1, oft auch Induktionsanfang genannt. (Auch wenn ich persönlich
diese Benennung für nicht notwendig halte.) Weil wir hier den Fall
betrachten, dass n1 = zero, müssen wir noch zeigen, dass für alle n2
und n3 gilt, dass 0 + (n2 + n3) = (0 + n2) + n3. Dazu führen wir nun
zunächst die beiden NAT's n2 und n3 mittels intros n2 n3 ein.
intros n2 n3.
Die Taktik simpl vereinfacht das Beweisziel. Dies ist hier möglich,
weil die Funktion add ausgewerted werden kann, weil die Struktur des
ersten Arguments (zero) bekannt ist.
simpl.
Da nun links und rechts das gleiche steht, gilt diese Gleichung. Dies
zeigen wir mittels reflexivity.
reflexivity.
-
-
Nun zu Fall 2, oft auch Induktionsschritt genannt. (Auch diese
Benennung halte ich persönlich für nicht notwendig.) Weil wir hier den
Fall betrachten, dass n1 = succ(n1'), müssen wir (unter Annahme der
IH) zeigen, dass (n1' + 1) + (n2 + n3) = ((n1' + 1) + n2) + n3 für
alle n2 und n3 gilt. Wir verfahren wie im ersten Fall:
intros n2 n3.
simpl.
simpl.
Durch specialize spezialisieren wir nun die IH auf unser aktuelles
Beweisziel. Mit rewrite IH können wir diese Gleichung zum Umschreiben
des Beweisziels verwenden.
Kurze Frage zum Nachdenken: Warum ist die IH allquantifiziert über
n2 und n3? Wäre das auch bei Pen&Paper-Beweisen der Fall?
specialize IH with (n2 := n2) (n3 := n3).
rewrite IH.
reflexivity.
Qed.
rewrite IH.
reflexivity.
Qed.
Beispiel: Kommutativität von add
Probieren wir uns an der Kommutativität von add. Auch hier ist Induktion der natürliche Weg.Theorem add_comm :
forall (n1 n2 : NAT),
add n1 n2 = add n2 n1.
Proof.
intros n1.
induction n1 as [|n1' IH].
-
intros n2.
simpl.
Fail reflexivity.
Klappt nicht. Hier brauchen wir ein Hilfslemma add_zero_r. Schauen wir
uns noch den schnell den anderen Fall an.
admit.
-
intros n2.
simpl.
specialize IH with (n2 := n2).
rewrite IH.
Fail reflexivity.
-
intros n2.
simpl.
specialize IH with (n2 := n2).
rewrite IH.
Fail reflexivity.
Noch ein Hilfslemma...
Abort.
Frage zum Nachdenken: Wieso ergibt es "Sinn", dass die nachfolgenden
Lemmatas nötig sind?
Typischerweise beginnt man Beweise per Induktion nicht mit "Sei ...",
sondern direkt mit "Beweis per Induktion über ...". Entsprechend brauchen
wir auch hier nicht intros n, um induction anwenden zu können.
induction n as [|n' IH].
Die beiden Teilbeweise sind zur Übung offen gelassen.
-
admit.
-
admit.
Admitted.
Lemma add_succ_r :
forall (n1 n2 : NAT),
add n1 (succ n2) = succ (add n1 n2).
Proof.
admit.
-
admit.
Admitted.
Lemma add_succ_r :
forall (n1 n2 : NAT),
add n1 (succ n2) = succ (add n1 n2).
Proof.
Wie sieht es hier aus?
Admitted.
Mit Hilfe der beiden Lemmata add_zero_r und add_succ_r können wir nun
einfach zeigen, dass add kommutativ ist.
Theorem add_comm :
forall (n1 n2 : NAT),
add n1 n2 = add n2 n1.
Proof.
induction n1 as [|n1' IH].
-
intros n2.
simpl.
Man kann rewrite mit jeder Form von Gleichung verwenden, also auch in
Kombination mit anderen Lemmata.
Disjunktive Korrektheit
Nun ein Beispiel mit Aussagenlogik. Wichtig ist es hier, dass wir wieder ein Theorem haben, das über einer induktiven Struktur allquantifiziert: "Für alle f : formula gilt...". Entsprechend bietet sich auch hier ein Beweis per Induktion an.
"Sei"
"Wenn"
"Dann"
Beweis per Induktion...
induction f as
Hier folgt jetzt das entsprechende Benennungsschema für die einzelnen
Fälle. Zum Beispiel besteht eine Disjunktion aus zwei Teilformeln, für die
jeweils eine Induktionshypothese benötigt wird.
[
|
|a'
|f1 IH1
|f1 IH1 f2 IH2
|f1 IH1 f2 IH2
|f1 IH1 f2 IH2
|f1 IH1 f2 IH2
].
-
-
intros a k H1 H2 H3.
top wird natürlich immer erfüllt (per Definition).
simpl.
trivial.
trivial.
Schauen wir uns aber noch einen anderen möglichen Beweis für diesen Fall
an.
Undo.
Wir können mit H3 einen Widerspruch herleiten, da top weder bottom
noch ein atom noch eine disjunction ist.
simpl in H3.
contradiction H3.
contradiction H3.
Und noch eine Möglichkeit:
Undo.
Da das Atom a nicht in top vorkommt, führt auch H2 zu einem
Widerspruch.
simpl in H2.
contradiction H2.
-
contradiction H2.
-
intros a k H1 H2 H3.
Hier führt nur ein Weg zum Ziel. Welcher?
admit.
-
-
intros a k H1 H2 H3.
Wir wissen per H2, dass das Atom a in atom (a') vorkommt.
Vereinfachen wir doch zunächst H2. Also muss a' = a gelten.
simpl in H2.
Die Taktik subst a' ersetzt a' im gesamten Kontext durch a, der
rechten Seite der Gleichung H4.
subst a'.
simpl.
exact H1.
-
simpl.
exact H1.
-
intros a k H1 H2 H3.
simpl in H3.
contradiction H3.
-
contradiction H3.
-
intros a k H1 H2 H3.
simpl in H3.
contradiction H3.
-
contradiction H3.
-
intros a k H1 H2 H3.
Um zu zeigen, dass disjunction f1 f2 von k erfüllt wird, müssen wir
zeigen, dass f1 oder f2 von k erfüllt werden. So ist zumindest
satisfaction definiert.
simpl.
Leider wissen wir an der Stelle noch nicht, ob f1 oder f2 von k
erfüllt wird. Wir müssen also woanders weitermachen.
Wir können zunächst H3 auseinanderbauen. Da disjunction (f1) (f2)
eine Disjunktion ist ist, wissen wir auch, dass f1 als auch f2 nur
bottom, ein atom oder eine disjunction sein können.
simpl in H3.
destruct H3 as [H31 H32].
destruct H3 as [H31 H32].
Da das Atom a in disjunction (f1) (f2) vorkommt, muss es mindestens
in f1 oder f2 vorkommen. Dieses Oder können wir wegen der Definition
von atom_occurs sichtbar machen.
simpl in H2.
Mit destruct bekommen wir jetzt eine entsprechende Fallunterscheidung.
destruct H2 as [H21|H22].
+
+
Fall: Das Atom a kommt in f1 vor.
Nun kann man bereits erkennen, dass f1 von k erfüllt wird.
left.
Hierfür brauchen wir die Induktionshypothese IH1 für f1.
specialize IH1 with (a := a) (k := k).
apply IH1.
*
exact H1.
*
apply IH1.
*
exact H1.
*
Was fehlt hier noch?
admit.
*
*
Was fehlt hier noch?
admit.
+
+
Wie funktioniert der zweite Fall?
admit.
-
-
intros a k H1 H2 H3.
Wie geht es jetzt weiter?
admit.
-
-
admit.
Admitted.
Admitted.
Fazit: Induktionsbeweise über größeren Strukturen wie z.B. formula sind
deutlich ausführlicher und erfordern eventuell komplexere Zwischenschritte in
den einzelnen Teilbeweisen. Das Grundprinzip ist aber das gleiche geblieben!
Wir haben einen Teilbeweis pro Konstruktur, und pro Teilbeweis so viele
Induktionshypothesen zur Verfügung, wie der Konstruktor stellig ist!