Gymnasium Ottobrunn Kollegstufe 2006/2008 Seminarfach Mathematik/Informatik Seminararbeit Rekursion: Prinzip, Verifikation und Umsetzung als funktionales Programm Verfasser: Christian Münch Kooperationspartner: Martin Hofmann, LMU München Kursleiter: Peter Brichzin Bewertung:…… Punkte Unterschrift des Kursleiters:……………………….. Gliederung: 1 Hinführung zum Thema ..................................................................................................... 3 2 Rekursion ........................................................................................................................... 4 2.1 Rekursives Prinzip bei Funktionen ............................................................................ 4 2.2 Summenfunktion ........................................................................................................ 4 2.3 Rekursive Datenstrukturen ......................................................................................... 6 3 Beweistechnik vollständige Induktion ............................................................................... 9 3.1 Prinzip der vollständigen Induktion ........................................................................... 9 3.2 Gaußsche Summe ....................................................................................................... 9 3.3 Türme von Hanoi ..................................................................................................... 10 4 Funktionale Programmierung mit Haskell ....................................................................... 13 4.1 Vergleich imperative Programme - funktionale Programme ................................... 13 4.2 Funktionsprinzip von Haskell .................................................................................. 13 4.3 Summenfunktion und Kuerzerals-Funktion ............................................................. 14 4.4 Listenmaximum........................................................................................................ 16 4.5 Binärer Baum in Haskell .......................................................................................... 17 5 PVS................................................................................................................................... 19 5.1 Entstehung ................................................................................................................ 19 5.2 Funktionsprinzip....................................................................................................... 19 5.3 Beweis der geschlossenen Form der Summenfunktion............................................ 20 6 Insertsort........................................................................................................................... 25 6.1 Strategie und funktionale Programmierung ............................................................. 25 6.2 Beweise über Insertsort ............................................................................................ 26 6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen ........................ 27 6.2.2 Beweis für den Erhalt der Länge einer Liste nach dem Sortiervorgang .......... 28 6.2.3 Beweis für den Erhalt der Elemente einer Liste nach dem Sortierverfahren ... 30 6.2.4 Beweis für die Sortierung einer Liste durch Insertsort..................................... 34 7 Anhang ............................................................................................................................. 36 7.1 Datentypen und Operatoren...................................................................................... 36 7.2 Programmcode: Listenmaximum ............................................................................. 36 7.3 Beweise in PVS ........................................................................................................ 37 7.3.1 Beweis für den Erhalt der Länge einer Liste nach dem Sortierverfahren ........ 37 7.3.2 Beweis für den Erhalt der Elemente nach dem Sortierverfahren ..................... 42 7.3.3 Beweis für die Sortierung der Liste durch Insertsort ....................................... 51 8 Zusammenfassung ............................................................................................................ 57 9 Literaturverzeichnis.......................................................................................................... 58 10 Erklärung .......................................................................................................................... 59 1 Hinführung zum Thema Wer kennt es nicht? Ein Bild wie das Titelbild auf dem Deckblatt, das in sich wieder das gleiche Bild enthält. Schwer vorstellbar, dass sich dieses Bild ins unendliche fortsetzt, aber man findet kein Ende, egal wie weit man in das Bild hineinschaut. Trotzdem gibt es ein Ende oder anders gesagt, vielleicht eher einen Anfang. Dazu braucht man nur ein Bild (im Bild) zu nehmen und von diesem, als Ausgangspunkt, immer das nächst größere zu betrachten. Das Ende ist hier, wie leicht zu finden, das ursprüngliche, große Bild, von dem aus man vorher immer weiter nach innen geschaut hat.1 Dieses Phänomen der Rekursion, dass sich ein Problem durch sich selbst definiert, ist Thema der Seminararbeit. In diesem Rahmen werden das Prinzip, die Umsetzung als funktionales Programm und die Verifikation behandelt. Der Leser braucht zum Verstehen der Arbeit keine speziellen Kenntnisse aus der Mathematik. Die Fähigkeit abstrakt zu denken und ein allgemeines Grundverständnis der Mathematik reichen aus, um den Inhalt erfassen zu können.2 1 vgl. StR Peter Brichzin: Informatik am Gymnasium–Nachqualifikation von Lehrkräften (SIGNAL) - Rekursives Problemlösen, 1.0. Rekursion, was ist das?, Einführungskurs für Nichtmathematiker, Akademie für Lehrerfortbildung und Personalführung, Dillingen, 13.9/14.9.2004 2 vgl. Nicolas Bourbaki: Elements de Mathematique - Algebre Commutative, V To The Reader Addison-Wesley, Publishing Company, Paris 1972 3 2 Rekursion Rekursion ist die Definition eines Problems, einer Funktion oder eines Verfahrens durch sich selbst.3 2.1 Rekursives Prinzip bei Funktionen Hierbei wird das Ergebnis eines Verfahrens für eine bestimmte Eingabe auf das Ergebnis desselben Verfahrens zurückgeführt. Man nennt diesen Vorgang den Rekursionsschritt. Das Ergebnis wird nun wieder nach diesem Verfahren auf ein weiteres Ergebnis desselben Verfahrens zurückgeführt usw. Damit der Vorgang enden kann, muss für einen Eingabewert das Ergebnis bekannt bzw. definiert sein. Dieser Wert ist der Rekursionsanfang. Nun kann die Lösung der Funktion für den ursprünglichen Eingabewert leicht berechnet werden. Voraussetzung für den Ablauf eines rekursiven Programms ist, dass seine Funktionen sich selber aufrufen können. Die Anzahl der geschachtelten Aufrufe nennt man Rekursionstiefe. Der Rechenweg einer Funktion vom Eingabewert bis zum Rekursionsanfang weist also eine Rekursionstiefe entsprechend der Anzahl der Rekursionsschritte auf. Der Vorteil der rekursiven Darstellung einer Funktion sind die Kürze, da das Verfahren nur einmal definiert werden muss und für alle Rekursionsschritte gilt sowie die leichte Verständlichkeit, die die charakteristischen Eigenschaften einer rekursiv definierten Funktion mit sich bringen. Daher wird das rekursive Prinzip in vielen Programmiersprachen und anderen Bereichen der Informatik benutzt.4 2.2 Summenfunktion Ein einfaches Beispiel für eine rekursiv definierte Funktion ist die Berechnung der Summe der natürlichen Zahlen von 0 bis 10. Hierbei ist die Funktion „summe“ gemäß dem rekursiven Prinzip durch eine Fallunterscheidung definiert: , für n=0 summe(n) = 0 summe(n−1)+ n, sonst { Re kusionsanfang Re kursionsschritt n ∈ ℕ0 3 Von Meyers Lexikonredaktion: Duden Informatik-Fachlexikon für Studium und Praxis, Rekursion, Dudenverlag, 3.Auflage, Mannheim 2001 4 Von Meyers Lexikonredaktion: Duden Informatik-Fachlexikon für Studium und Praxis, Rekursion, Dudenverlag, 3.Auflage, Mannheim 2001 4 Die Generierung des Ausgabewerts kommt folgendermaßen zustande: summe(10) = summe(9) + 10 = summe(8) + 9 + 10 = … =0+ 1 + 2 + 3 + 4 + … + 10 = 55 Die Rekursionstiefe ist in der summe(10) gleich 10, was der Anzahl der verschachtelten Aufrufe, also 10 Rekursionsschritten, entspricht. Die Rekursionstiefe des Rekursionsanfangs ist 0. Bei Aufruf der Funktion für ein bestimmtes n wird die Funktion solange rekursiv vereinfacht, bis man die summe von 0 erhält, für die der Ausgabewert 0 definiert ist. Anschließend werden alle einzelnen Ausgabewerte addiert und das Ergebnis als Ausgabewert geliefert. Die Generierung wird im Folgenden durch ein Struktogramm veranschaulicht. Struktogramm: Summenfunktion summe(n) if n=0 ja nein 0 summe(n-1)+n Rekursionsanfang (Rekursionsschritt) Hier wird besonders die Fallunterscheidung der rekursiv definierten Summenfunktion deutlich. Ein Fall tritt ein, falls der ihm zugeordnete Wahrheitswert eintritt. Der Wahrheitswert ja für n = 0 beendet die Generierung, da der Rekursionsanfang aufgerufen wird, nein sorgt für den erneuten Aufruf des Programms in einem Rekursionsschritt. 0 0 Datenflussdiagramm summe summe (0) Re kursionsanfang } Rekursionstiefe = n ... n −1 Eingabewert } Re kursionstiefe = 1 summe n } Re kursionstiefe = 0 summe Ausgabewert Im Datenflussdiagramm wird der Generierungsprozess des Programms deutlich. „summe“ braucht immer zwei Eingabewerte. Diese werden addiert. Bei der rekursiv definierten Funktion wird zum Eingabewert n eine neue „summe“ addiert. Diese hat als ersten Eingabewert n-1, als zweiten wieder eine „summe“. So geht es bis zur „summe(0)“ weiter, für 5 die der feste zweite Eingabewert 0 addiert wird. Nun wird von oben nach unten ausgerechnet und der Ausgabewert ist generiert. Die Rekursionstiefe wird im Datenflussdiagramm gut veranschaulicht. Mit jeder Stufe steigt sie von unten nach oben um den Wert 1, angefangen bei 0. Daher ist die Rekursionstiefe des Rekursionsanfangs immer gleich dem Eingabewert n. 2.3 Rekursive Datenstrukturen Das rekursive Prinzip ermöglicht neben Funktionsdefinitionen auch das Zusammenfassen gleicher bzw. unterschiedlicher Datentypen (siehe auch „7.1 Datentypen und Operatoren“), nach bestimmten Konstruktionsprinzipien, zu so genannten „Datenstrukturen“. Hierbei sind verschiedene Zusammenfassungen denkbar. Zunächst können Daten gleichen Typs in einem ein- bis n-dimensionalen Feld zusammengefasst werden, wobei diese als Feldelemente bezeichnet werden. Jedes Feldelement erhält je nach Dimension ein, zwei, bzw. n Ordnungszahlen/Indizes. Diese Art von Datenstruktur nennt man Feld (array). Beispiele sind, wie aus der Mathematik bekannt, Koeffizienten eines Gleichungssystems, als Matrix dargestellt, oder auch Punkte in einem Koordinatensystem, die nach einer bestimmten Funktionsvorschrift ermittelt werden können. Hierbei wäre die Funktion f ( x) → x ²; x ∈ ℝ denkbar. X-Werte werden quadriert zu den Funktionswerten, also den y-Werten. So erhält jeder Punkt P zwei Indizes: P ( x / y ) , da er in einem zweidimensionalen Feld, dem kartesischen Koordinatensystem, liegt. Mehrere Datenfelder können nun zu einem Datensatz zusammengefasst werden. Es handelt sich also um die Zusammenstellung von Daten unterschiedlichen Datentyps, wie es beispielsweise in den Personalien, also Name, Adresse usw., einer Person verwirklicht ist. Diese Art von Datenstruktur heißt „Verbund“. Eine Möglichkeit so einen Verbund darzustellen ist die Liste (siehe auch „4.3 Summenfunktion und Kuerzerals-Funktion“): personalie1 = [[name1], [adresse1]] personalie2 = [[name2], [adresse2]] Schon hier wird das rekursive Prinzip deutlich, denn ein Feldelement befindet sich in einem Feld, dieses wiederum in einem Verbund. Verbund = [ Feld1, Feld 2] = [[ f 1, f 2, f 3], [ f 4, f 5, f 6]]; fi := Feldelement 6 Daten gleicher Struktur, also zum Beispiel Felder oder Verbunde, können nun wieder zu Files zusammengefasst werden. Dabei kann in einem File jede Datenstruktur, außer einem File selbst, gespeichert werden. Für die Daten gibt es unterschiedliche „Organisationsformen“. Wichtig ist auch, dass ein File aufgrund dieser Organisationsformen ständig erweitert werden und anschließend unter einem Namen auf einem Datenträger gespeichert werden kann. Das rekursive Prinzip in den Organisationsformen wird in der Datenstruktur „Baum“ deutlich. Die zusammengefassten Daten stehen hier nicht alle auf gleichem Niveau, es gibt über- und untergeordnete Daten. Dieses Prinzip wird nicht nur zur Organisation von Daten innerhalb eines Files gebraucht, sondern vor allem auch zum Ordnen von Files nach einem bestimmten Prinzip. Zum Beispiel können Files nach Datum geordnet werden. Hierbei wäre es denkbar, dass die Files zunächst nach dem Jahr, auf einem niedrigeren Niveau schließlich nach Monat, Tag und gegebenenfalls nach Uhrzeit geordnet werden. Dies macht zum Beispiel bei digitalen Fotos Sinn: 2007 Wurzel Kante Jan Feb Knoten 15 Foto1 20 Foto2 10 Blatt (letzte Information über Foto) Foto3 Die Wurzel des Baums ist hierbei das Datum, das keinen Vorgänger hat, zum Beispiel das Jahr, sofern nur Daten eines Jahrs geordnet werden. Über die so genannten Kanten gelangt man nun zu den Knoten des Baums. Die Daten eines Unterniveaus stehen immer unter dem nächst höheren Niveau, auf gleicher Höhe. Diese Knoten können also Monate, Tage usw. sein. Der Baum setzt sich bis zur „komplexesten Information“ fort. Es folgt nun kein niedrigeres Niveau mehr. Knoten dieses Typs nennt man Endknoten bzw. Blätter. Die Wurzel des Baums entspricht dem Rekursionsanfang. Durch einen Rekursionsschritt gelangt man von einem Knoten zum jeweils übergeordneten Niveau. 7 Demnach entspricht die Rekursionstiefe immer der Anzahl der Kanten, die auf dem Weg vom niedrigsten Blatt zur Wurzel des Baums zurückgelegt werden müssen. Wie schon aus den Beispielen ersichtlich geworden ist, ist das rekursive Prinzip also eine mächtige Form, Daten eines Computers unkompliziert zu ordnen. Durch eine rekursiv definierte Funktion wäre es nun möglich, zum Rekursionsanfang, der Wurzel, zu gelangen. Denn ein Blatt setzt sich z.B. zusammen aus einem Rest und einem Tag. Der Rest setzt sich aus einem neuen Rest und einem Monat zusammen. So geht es bis zur Wurzel weiter. Nun ist also der komplette zeitliche Hintergrund des untersuchten Files bekannt. Hierbei wird besonders deutlich, dass ein Rekursionsanfang essentiell für die Funktionsfähigkeit eines rekursiv definierten Programms ist. Als Beispiel wäre hier ein Programm denkbar, das Files, wie im Modell der Fotosortierung oben, ihrem Erstellungsdatum nach anordnet. Ohne Rekursionsanfang wäre es für das Programm verständlicherweise überhaupt nicht möglich, einen Zusammenhang zwischen den Ästen herzustellen und könnte sie nicht sortieren.5 5 von Meyers Lexikonredaktion: Duden Informatik Lehrbuch S II – Gymnasiale Oberstufe, Duden Paetec Schulbuchverlag, 1.2.3. Datenstrukturen 8 3 Beweistechnik vollständige Induktion 3.1 Prinzip der vollständigen Induktion Die vollständige Induktion ist eine Beweistechnik in der Mathematik und kann häufig auch in der Informatik eingesetzt werden. Zunächst wird bei dieser Technik die Behauptung für einen Initialfall, den man als Induktionsanfang bezeichnet, bewiesen. Daraufhin wird bewiesen, dass die Behauptung, wenn sie für die Induktionsannahme, also beispielsweise eine Variable n gilt, auch für den nächsten Schritt n+1, den so genannten Induktionsschritt, gültig ist. 3.2 Gaußsche Summe Als Einführungsbeispiel wird ein Beweis über die Summenfunktion erläutert. Zu beweisen ist die geschlossene Form der Gaußschen Summe: summe(n) = n * (n + 1) 2 Diese Form macht es möglich, ohne den langen Generierungsvorgang der rekursiven Definition, sofort ein Ergebnis zu berechnen. Zur Vereinfachung werden mathematische Zeichen benutzt. 10 Die Summe von 0 bis 10 schreibt man also: ∑ i . i =0 Gelesen: „Die Summe über i von i gleich 0 bis10.“ Behauptung: n n *(n + 1) ;i ∈ ℕ0 ∑ i= 2 i =0 Beweis durch vollständige Induktion: (1) Induktionsanfang: 0 0* (0 + 1) =0 ∑ i= 2 i =0 q.e.d. (2) Induktionsschritt: a +1 (a + 1) *(a + 2) a a *(a + 1) Zu zeigen: ∑ i = , falls ∑ i = 2 2 i =0 i =0 9 Es gilt: a+1 a ∑ i = ∑ i + (a + 1) (nach rekursivem Pr inzip) i =0 i =0 a * (a + 1) = + (a + 1) (Voraussetzung durch Induktionsschritt eingesetzt ) 2 a (a + 1) * (a + 2) = (a + 1) * + 1 = 2 2 q.e.d. Damit ist bewiesen, dass die Funktion für n+1 gilt, wenn sie für n gilt. Da sie aber schon für 0 erfüllt wird, gilt sie auch für alle n. Die geschlossene Form der Summenfunktion ist also für alle n ∈ ℕ 0 gültig.6 3.3 Türme von Hanoi Ein weiteres Beispiel ist ein Beweis zu den Türmen von Hanoi. Bei dieser Anordnung gibt es n Scheiben verschiedener Größe, die von unten nach oben immer kleiner werdend, auf einem Stab angeordnet sind. Diese Scheiben sollen nun auf einen anderen Stab geordnet werden. Hierbei darf keine große auf eine kleinere Scheibe gelegt werden. Neben den beiden schon erwähnten Stäben gibt es noch einen dritten, auf dem die Scheiben abgelegt werden können. Behauptung 1: Umschichten in N (n) = 2n − 1 Schritten möglich Beweis: (1) Induktionsanfang: N (1) = 21 − 1 = 1 q.e.d. (2) Induktionsschritt: Zu zeigen: N (a + 1) = 2a +1 − 1, falls N (a ) = 2a − 1 Die Anzahl N (a + 1) entspricht dem zweimaligen Wegschichten der obersten a Scheiben, einmal, um die unterste Scheibe frei zuräumen und das zweite Mal zum Aufschichten auf die verschobene unterste Scheibe. Außerdem muss noch die unterste Scheibe verschoben werden. 6 Otto Forster: Analysis 1-Differential- und Integralrechnung einer Veränderlichen, 1.Vollständige Induktion, Friedrich Vieweg & Sohn Verlag, 7.Auflage, Wiesbaden 2004 10 Insgesamt ergibt sich also als Gleichung: N (a + 1) = 2 * ( N (a) − 1) + 1 = 2 * 2a − 2 + 1(Voraussetzung durch Induktionsschritt eingesetzt ) = 2 * 2a − 1 = 2a +1 − 1 q.e.d. Behauptung 2: Mindestens 2n − 1 Schritte nötig, um die Scheiben umzuschichten. Beweis: (1) Induktionsanfang: N (1) ≥ 21 − 1 N (1) ≥ 1 q.e.d. (2) Induktionsschritt: Zu zeigen: N (a + 1) ≥ 2a +1 − 1, falls N (a ) ≥ 2a − 1 Diese Anzahl N (a + 1) entspricht dem zweimaligen Wegschichten der obersten a Scheiben, einmal, um die unterste Scheibe frei zuräumen und das zweite Mal zum Aufschichten auf die verschobene unterste Scheibe. Um die obersten a Scheiben umzuschichten sind jeweils wieder mindestens die N (a ) Schritte nötig. Mindestens deshalb, weil nach Behauptung für n gleich a mindestens 2a − 1 Schritte dafür nötig sind. Außerdem muss noch die unterste Scheibe verschoben werden. Auch dafür ist mindestens 1 Schritt notwendig Insgesamt ergibt sich also als Ungleichung: N (a + 1) ≥ 2 * ( N (a ) − 1) + 1 ⇒ N (a + 1) ≥ 2 * 2a − 2 + 1(Voraussetzung durch Induktionsschritt eingesetzt ) ⇒ N (a + 1) ≥ 2 * 2a − 1 ⇒ N (a + 1) ≥ 2a +1 − 1 q.e.d. Damit ist also insgesamt bewiesen, dass einerseits das Umschichten in 2n − 1 Schritten möglich ist, andererseits aber auch genau so viele Schritte dafür nötig sind. 11 Anwendung findet diese Art von Beweis in der Informatik, indem einerseits die Terminierung von rekursiven Funktionen und andererseits die geschlossene Form von Funktionen gezeigt werden kann. Dies ist wichtig, um derartige Funktionen definieren zu können, denn wenn eine rekursive Funktion nicht terminiert, kann sie nicht zu einem Ende kommen und damit auch keinen Ausgabewert liefern. Außerdem ist es einfacher eine geschlossene Form als Zuordnungsvorschrift anzugeben, da die Funktion so nicht immer aufwendig und abschnittsweise definiert werden muss. Dies sieht man besonders gut am Beispiel der Gaußschen Summe, die einerseits als Fallunterscheidung und andererseits, nach dem Beweis der geschlossenen Form, nun auch in dieser einfacheren Form definiert werden kann. 12 4 Funktionale Programmierung mit Haskell 4.1 Vergleich imperative Programme - funktionale Programme Neben den eher bekannten, imperativen Programmiersprachen, zu denen beispielsweise Java zu einem gewissen Teil gehört (mit Java können auch funktionale Programme definiert werden) , bei denen Variablen und Wiederholungen (Schleifen) definiert werden, gibt es auch deklarative Programmiersprachen. Hierzu gehört unter anderem die funktionale Programmierung, bei der Funktionen als eine Art Problemlösungsstrategie definiert werden. Ein weiterer Unterschied zu imperativen Programmen ist, dass den Variablen nicht Werte zugewiesen werden, die dann, auf Grund der Definition, anderen Variablen Werte zuweisen, sondern dass Funktionen lediglich ausgewertet werden. Die Lösung erscheint dann als Ausgabewert7. 4.2 Funktionsprinzip von Haskell Haskell ist eine solche funktionale Programmiersprache. Benannt ist sie nach Haskell B. Curry (1900-1982). Er beschäftigte sich vornehmlich mit den mathematischen Grundlagen von funktionalen Programmiersprachen, also der kombinatorischen Logik und dem λ-Kalkül, einer formalen Sprache mit Hilfe derer man Funktionen definieren kann8. Der Programmcode wird im gewöhnlichen Text-Editor verfasst. Der Interpreter heißt Hugs 9. Beim Erstellen eines funktionalen Programms teilt man eine komplizierte Programmieraufgabe zunächst in kleinere Teilaufgaben auf, die durch Funktionen auszudrücken sind. Um eine Funktion zu definieren, muss der Datentyp des Ein- und Ausgabewertes angegeben werden. Es wird durch die Zuordnungsvorschrift definiert, auf welche Weise aus den Eingabewerten Ausgabewerte generiert werden. Das Programm entsteht durch Kombinationen solcher Funktionen, die wieder durch Funktionen miteinander in Beziehung gebracht werden10. 7 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 1 Einordnung funktionaler Programmierung, März 2003 8 http://de.wikipedia.org/wiki/Formale_Sprache, http://de.wikipedia.org/wiki/LambdaKalk%C3%BCl#Einf.C3.BChrung, 24.03.07 9 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 2 Haskell und Hugs, März 2003 10 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 1 Einordnung funktionaler Programmierung, März 2003 13 4.3 Summenfunktion und Kuerzerals-Funktion Als Einstiegsbeispiel eignet sich auch hier wieder die Summenfunktion. Im Editor definiert man: summe::Int → Int --„summe“ = Name der Funktion; Int = ℤ = Typangabe des Ein- und Ausgabewertes (weitere Typangaben: siehe „7.1 Datentypen und Operatoren“) summe 0 = 0 -- Definition der Funktion für den bestimmten Fall 0 summe n = summe(n − 1) + n -- Definition der Summe für n > 0 ; für n < 0 Fehlermeldung Die letzten beiden Zeilen ergeben also die schon oben beschriebene Fallunterscheidung, nur in anderer Syntax.11 Nun wird das Programm mit dem Dateityp „.hs“ gespeichert und in Hugs aufgerufen. Es erscheint ein „Main>“, hinter das eine Eingabe geschrieben wird. Die definierte Funktion kann durch den Funktionsnamen und den gewünschten Eingabewert aufgerufen werden. Um den Ausgabewert zu erhalten, muss man nun lediglich noch die Eingabetaste drücken, also zum Beispiel: Main> summe 10 55 Da im Folgenden weitere Funktionen mit Listen und Tupeln programmiert werden, müssen die beiden Begriffe zunächst definiert werden. Eine Liste ist eine Zusammenfassung beliebig vieler Objekte eines gemeinsamen Datentyps zu einem neuen strukturierten Objekt. Es gibt Listen verschiedenen Typs wie zum Beispiel Int-Listen, Bool-Listen usw. Eine Bool-Liste könnte hierbei so aussehen: [Bool, True, True] :: Bool Die Liste wird durch eckige Klammern gekennzeichnet und hinter den beiden Doppelpunkten wird der Typ angegeben, den alle Elemente der Liste haben. Im Gegensatz zur Liste gibt es Tupel. Es kann hier zwar nur eine feste Anzahl von Elementen zusammengefasst werden, die Elemente können jedoch unterschiedliche Typen haben. Ein Beispiel für ein Int-Bool-Tupel wäre: (1,True) :: (Int, Bool) 11 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 4 Funktionen als Programme, März 2003 14 Das Tupel wird mit runden Klammern gekennzeichnet, der Typ der Elemente steht wieder nach den beiden Doppelpunkten.12 Die folgende Beispielfunktion kann in Bezug auf zwei Listen entscheiden, ob die erste Liste, die eingegeben wird, kürzer ist als die zweite. Ausgegeben wird ein Wahrheitswert, also „True“ für wahr und „False“ für falsch: kuerzerAls:: [a] → [b] → Bool kuerzerAls [] [] = False kuerzerAls x [] = False kuerzerAls [] y = True kuerzerAls (x:xe) (y:ye) = kuerzerAls xe ye In der ersten Zeile wird wieder der Name der Funktion angegeben, hier „kuerzerAls“. Nach dem doppelten Doppelpunkt stehen zunächst die Typen der beiden Eingabewerte, mit einem Pfeil voneinander getrennt. Da der Typ der Listenelemente bei dieser Funktion nicht ausschlaggebend ist, können „a“ und „b“ dafür angegeben werden. Damit ist der Typ der eingegebenen Listen beliebig und kann sogar bei jeder Liste unterschiedlich sein. Hinter dem zweiten Pfeil steht der Typ des Ausgabewertes, hier „Bool“. Dies ist ohne Klammern möglich, weil es in der Funktionalen Programmierung, genau wie in der Mathematik, immer nur einen Ausgabewert geben kann. In der nächsten Zeile ist definiert, das bei zwei „leeren“ Listen als Eingabewert „False“ ausgegeben werden soll, da eine leere Liste ja nicht weniger Elemente enthalten kann, als eine andere leere Liste. Beide haben 0 Elemente. In der 3. Zeile steht, dass bei beliebiger Elementanzahl in der ersten Liste und leerer zweiter Liste auch der Wert „False“ ausgegeben wird. In Zeile 4 ist umgekehrt die erste Liste leer und die zweite Liste hat eine beliebige Anzahl von Elementen. Da die erste Liste logischerweise weniger Elemente besitzt, wird hier der Wert „True“ ausgegeben. Man könnte meinen, dass die zweite Zeile überflüssig ist, da die Liste „x“ ja auch leer sein könnte und somit der Fall zweier leerer Listen abgedeckt wäre, allerdings könnte bei dieser Argumentation auch die Liste „y“ leer sein. Damit würden beide definierten Regeln gleichzeitig in Kraft treten und es käme zum Widerspruch. Durch die zweite Zeile ist dieser besondere Fall aber genau definiert und die dritte bzw. vierte Zeile gilt nur noch, wenn die Liste „x“ bzw. „y“ mehr als 0 Elemente besitzt. In der letzten Zeile befindet sich schließlich die Definition für den allgemeinen Fall. Keine der beiden Listen ist leer. 12 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 5 Datenstrukturen, März 2003 15 Es wird vorgeschrieben, dass die Funktion „kuerzerAls“ von einer ersten Liste, die aus dem ersten Element „x“ und einer beliebigen Anzahl weiterer Elemente „xe“ besteht und einer zweiten Liste, die aus einem ersten Element „y“ und einer beliebigen Anzahl weiterer Elemente „ye“ besteht, je das erste Element entfernt und sich selbst wieder Aufruft. Allerdings sind die Eingabewerte nun, wie schon gesagt, lediglich noch „xe“ und „ye“. Das rekursive Prinzip ist in der Funktion deutlich zu erkennen. Nun wird wieder das erste Element beider Listen entfernt und die Funktion erneut aufgerufen. Dieser Vorgang wird solange wiederholt, bis entweder eine, oder beide Listen leer sind. Für diese Fälle sind in den vorherigen Zeilen Ausgabewerte definiert worden. Ein Beispiel für den Vergleich von zwei Listen in Hugs ist folgendes: Main> kuerzerAls ['H','a','l','l','o'] ['d', 'u'] False Das Besondere an diesem Beispiel ist, dass die Listen den Typ „Char“ haben, also einzelne Zeichen als Elemente besitzen. Diese werden durch Hochkommas gekennzeichnet. Neben diesen „Chars“ gibt es in Haskell auch noch „Strings“, die durch hochgestellte Anführungsstriche gekennzeichnet werden und die Zeichenketten, beispielsweise Wörter, darstellen. „Strings“ sind definiert als Listen von „Chars“. Aus diesem Grund könnte man den Funktionsaufruf von oben auch schreiben als: Main> kuerzerAls "Hallo" "du" False Es wird der gleiche Wert ausgegeben.13 4.4 Listenmaximum Die Funktion „listenmaximum“ ist so definiert, dass sie aus einer Int-Liste als Eingabe das größte Element auswählt und dieses anschließend ausgibt. Um die Geschwindigkeit des Programms zu erhöhen, wird hierbei ein so genannter Akkumulator verwendet, der die Zwischenergebnisse speichert und so ein schnelles Weiterrechnen ermöglicht. Zunächst muss definiert werden, dass bei einer Liste mit nur einem Element dieses das größte ist. Für alle anderen Fälle ruft die Funktion die Hilfsfunktion „hlistenmaximum“ auf, wobei zu der Liste als erster Eingabewert automatisch noch der weitere Eingabewert „0“ angefügt wird. Dieser entspricht dem Akkumulator zu Beginn. In der Hilfsfunktion muss nun zunächst der Fall definiert sein, dass die zu untersuchende Liste leer ist und der Akkumulator gleich 0 ist. In dieser Situation gibt die Funktion eine Fehlermeldung aus, die lautet: "Leere Liste hat kein Maximum". 13 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 5.3.1 Listenverarbeitung, Aufgabe 9, März 2003 16 Als nächstes wird der Fall definiert, dass die Liste nur noch ein Element besitzt, der Akkumulator aber einen beliebigen Wert haben kann. Dieser wird deshalb durch „acc“ als Variable ausgedrückt. Als letzter Fall wird nun noch eine Fallunterscheidung definiert: Wenn die Liste mehrere Elemente besitzt - der Wert des Akkumulators ist hierbei wieder variabel – dann vergleicht die Funktion die ersten beiden Werte. Der kleinere Wert wird ausgeworfen und die Funktion ruft sich selbst wieder auf, wobei die Liste als Eingabe um einen Wert kürzer geworden ist. Der Akkumulator nimmt nun den Wert des größeren Elementes aus dem Vergleich, also entweder des ersten oder des zweiten Wertes aus der ursprünglichen Liste, an. Dieser Vorgang wird solange wiederholt, bis nur noch eine Liste mit einem Element als Eingabewert übrig ist. Der Akkumulator entspricht diesem Element. Der Ausgabewert ist der Akkumulator. Dies entspricht wieder dem rekursiven Prinzip, da die Funktion, als Fallunterscheidung definiert, sich selbst solange aufruft (Rekursionsschritte), bis nur noch ein Element in der Liste ist. Für diesen Fall wurde bereits ein bestimmter Ausgabewert, der des Akkumulators zu diesem Zeitpunkt, definiert (Rekursionsanfang). Damit hat die Hilfsfunktion das größte Element der Liste bestimmt. Die Rekursionstiefe entspricht hierbei immer der Anzahl der Elemente in der Liste zu Beginn. Der Programmcode zu dieser Funktion ist in „7.2 Programmcode: Listenmaximum“ zu finden.14 4.5 Binärer Baum in Haskell Haskell bietet unter anderem den Vorteil, dass durch die Möglichkeit, mit Listen zu arbeiten, auch binäre Bäume erstellt werden können. Diese werden folgendermaßen rekursiv definiert: [knoten, links, rechts]:: Num a ⇒ [a] Der Baum ist also eine Liste von Elementen des Typs „a“. „knoten“ ist als Variable für den ersten Knoten des Baumes definiert. „links“ ist der Ast des binären Baumes, der nach links zum nächsten Knoten führt. Das Analogon gilt für „rechts“. Die beiden Äste sind rekursiv definiert und haben wieder den gleichen Aufbau. Man könnte also die Liste von oben um einen Schritt „ausdehnen“: [knoten, links, rechts]=[knoten, [knoten1, links1, rechts1], [knoten2, links2, rechts2]] 14 vgl. Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap. 5.3.1 Listenverarbeitung, Aufgabe 7, März 2003 17 Der Rekursionsanfang des Baumes ist nach „2.3 Rekursive Datenstrukturen“ seine Wurzel, also „knoten“. Der Rekursionsschritt ist der Schritt von einem zum nächsten Knoten über eine Kante. Auch hier ist die Rekursionstiefe gleich der Anzahl der Rekursionsschritte. Es ist in Haskell zum Beispiel möglich ein Programm zu programmieren, dass eine Int-Liste erst in einen Baum umwandelt und diesen anschließend, angefangen beim kleinsten Wert, links unten, zu sortieren. Dieses Programm besteht aus drei Funktionen: baumliste:: [Int]->[Int]->[Int]->[Int] --Name der Funktion; Typ der Ein- und Ausgabewerte baumliste x y z = y ++ x ++ z --fügt die sortierten Listen zu einer neuen Liste zusammen listebaum:: [Int]->[Int]->[Int]->[Int]->[Int] --Name der Funktion; Typ der Ein- und Ausgabewerte listebaum x [] links rechts = baumliste x links rechts --zweiter Eingabewert ist leere Liste; links/rechts sind Akkumulatoren; Funktion baumliste wird aufgerufen, Sortierverfahren endet listebaum x y links rechts --allgemeiner Fall: Fallunterscheidung |((head x)>=(head y)) = listebaum x (tail y) (listebaum [head y] links [] []) rechts --x ist größer oder gleich erstes Element von y, dieses wird links eingeordnet |((head x)<(head y)) = listebaum x (tail y) links (listebaum [head y] rechts [] []) --x ist kleiner als erstes Element von y, dieses wird rechts eingeordnet baumsort:: [Int]->[Int] --Name der Funktion; Typ der Ein- und Ausgabewerte baumsort [] = [] --leere Liste kann nicht sortiert werden, leere Liste ausgegeben baumsort (x:y) = listebaum [x] y [] [] --allgemeiner Fall: listebaum wird aufgerufen 18 5 PVS 5.1 Entstehung Das Beweissystem PVS wurde bei SRI, in Menlo Park, in Californien, von Owre, Rushby und Shankar entwickelt.15 5.2 Funktionsprinzip PVS steht für „Prototype Verification System“. Es handelt sich dabei um ein Programm, das bei einer Vielzahl von Beweisen helfen kann, da es nur legale Beweisschritte zulässt und so ausschließlich korrekte Beweise erstellt werden können. Zunächst muss eine Theorie durch Regeln, also Axiome, in Form von Beziehungen zwischen Funktionen und richtigen Aussagen über den Sachverhalt definiert werden. Dies entspricht dem Aufstellen von Voraussetzungen für einen Beweis. Anschließend wird ein so genanntes Theorem erstellt, das es zu beweisen gilt. Es entspricht der Behauptung des Beweises. Dies kann beispielsweise die geschlossene Form der Theorie/Funktion oder auch eine andere, allgemein gültige Tatsache in Bezug auf die Axiome sein. Nun wird der so genannte Prover aufgerufen. Mit Hilfe von Befehlen kann man die Aussagen, die der Prover aufgrund des Theorems aufstellt, auf mathematisch legale Weise solange umformen, bis alle übereinstimmen bzw. sich nicht ausschließen. Damit ist das Theorem bewiesen. Wichtig sind solche Beweise vor allem in der Informatik, da hier nur durch inhaltlich und formal korrekten Programmcode ein Programm erstellt werden kann, in dem keine Fehler auftreten. Anwendung findet PVS in akademischen, sowie geschäftlichen Bereichen, da es durch sein umfangreiches Spektrum an Typen, dadurch, dass man die Theorien mit Hilfe von Parametern formuliert und nicht zuletzt durch die Möglichkeit, abstrakte Typen wie Listen und Bäume zu definieren, im Bereich der höheren Ordnung eingesetzt werden kann. Beweisprogramme, wie dieses, sind aus der Informatik nicht wegzudenken, da die meisten Systeme einen Umfang erreicht haben, der manuell nicht mehr zu überprüfen ist. In diesem Fall hilft PVS dem Benutzer unter anderem durch eine Sammlung von bereits bewiesenen Theoremen weiter.16 17 15 vgl. Martin Hofmann: Vorlesungsskript Rechnergeschütztes Beweisen, 1 Introduction, 1.3 Proof assistants, 1.3.1 The PVS System, Wintersemester 2005/2006 16 vgl. Martin Hofmann: Vorlesungsskript Rechnergeschütztes Beweisen, 1 Introduction, 1.3 Proof assistants, Wintersemester 2005/2006 17 vgl. Sam Owre, Dr. John Rushby, Dr. Natarajan Shankar, Judy Crow & Mandayam Srivas, A Tutorial Introduction to PVS, From Workshop on Industrial-Strength Formal Specification Techniques, II. Tutorial on Using PVS, 1 Introducing PVS, Boca Raton, Florida, April 1995 19 5.3 Beweis der geschlossenen Form der Summenfunktion Wie schon in „3.2 Gaußsche Summe“ soll auch hier die geschlossene Form der Summenfunktion bewiesen werden. Hierbei ist der Vergleich zum mathematischen Beweis durch vollständige Induktion interessant, denn auch in PVS tritt das Prinzip der vollständigen Induktion auf. Mit diesem Hintergedanken fällt es gegebenenfalls leichter, den Beweis zu verstehen, da die Befehle und die Syntax zu Beginn etwas verwirrend seien können. Die Definition der Funktion „sum“ in PVS lautet wie folgt: sum: THEORY BEGIN / n ∈ ℕ0 n: VAR nat sum(n): RECURSIVE nat = (IF n=0 THEN 0 ELSE n+sum(n-1) ENDIF) MEASURE (LAMBDA n:n) closed_form: THEOREM sum(n) = (n*(n+1))/2 END sum In der ersten Zeile wird ausgedrückt, dass eine Theorie folgt, die hier „sum“ genannt wird. Anschließend kennzeichnet man durch „BEGIN“ den Anfang der Theorie. Die Summenfunktion „sum von n“ in Zeile vier hat den Typ der natürlichen Zahlen. Außerdem wird durch „RECURSIVE“ die rekursive Definition der Funktion angekündigt. Nun folgt das Axiom der Fallunterscheidung, n gleich bzw. ungleich Null. Die Funktion wird rekursiv definiert. In der nächsten Zeile steht, dass die Funktion aus einem Eingabewert des Typs n, also der natürlichen Zahlen, wieder einen Wert des Typs n generiert. Nun folgt das zu beweisende Theorem. Es hat den Namen „closed_form“, was seinen Sinn für den Leser verdeutlicht. Man hätte es allerdings auch beliebig anders benennen können. Das Theorem besagt: sum(n) = n * (n + 1) / 2 Zum Schluss wird das Ende der Theorie mit „END“ und dem Namen der Theorie gekennzeichnet. Ablauf des Beweises: Der Cursor wird zum Starten des Beweises auf die Zeile des Theorems gesetzt. Anschließend wird die Tastenkombination alt + m + x prove [Enter] eingegeben werden. PVS überprüft im ersten Schritt, ob die Syntax richtig ist. Diesen Vorgang nennt man „parse“. Anschließend führt es den Vorgang „typecheck“ durch, es wird also getestet, ob die Definitionen sich vom Typ her ausschließen oder ob die Typen der verschiedenen Teile 20 überhaupt definiert sind. In diesem Test ergeben sich so genannte „TCC’s“, also Formeln, die anschließend im Prover überprüft werden. Im Fall der Summenfunktion von oben ergeben sich zwei TCC’s. Hierbei ist zunächst zu beweisen, dass bei der Fallunterscheidung nur Ergebnisse des Typs der natürlichen Zahlen herauskommen können, da die Funktion sum(n) diesen Typ haben muss. Dafür ist es allerdings notwendig, dass der Zweig der Fallunterscheidung, in dem die Subtraktion vorkommt nur für n ≠ 0 gilt. Sonst würde sich ein negativer Wert ergeben und der „typecheck“ könnte nicht abgeschlossen werden, also einen Fehler melden. Als zweites muss durch die rekursive Definition der Funktion deren Terminierung sichergestellt werden, sonst wird das PVS überfordert und kann den Beweis auch nicht als richtig anerkennen. Um die beiden Tatsachen zu beweisen, öffnet PVS nun ein Fenster, in dem über einer gestrichelten Linie die „andecedents“ und darunter die „succedents“ aufgelistet sind. Diese Formeln nennt man „sequents“. Insgesamt kann ein solches „sequent“ so aufgefasst werden, dass die logische UND-Verknüpfung der „andecedents“ die logische ODER-Verknüpfung der „succedents“ impliziert. Die Aussagen über der Linie entsprechen also der Voraussetzung, die Aussagen unter der Linie der Behauptung des Beweises. Zu beweisen sind demnach lediglich die succedends. Im Beispiel der Summenfunktion ergibt sich zunächst nur ein „succedent“: closed_form : |-----{1} (FORALL(n: nat): sum(n) = (n*(n+1))/2) Rule? PVS fordert den Benutzer durch „Rule?“ dazu auf, die Formel durch vordefinierte Befehle umzuformen und dadurch den Beweis fertig zu stellen. Hierbei wird der Beweis automatisch in Unterbeweise aufgeteilt. Immer wenn ein Teil bewiesen ist, ruft PVS automatisch den nächsten Zweig auf, bis letztendlich alle Unterteile und damit der ganze Beweis fertig sind. Im Fall der Summe erfolgt der Beweis wie gesagt durch vollständige Induktion. Befehle werden in PVS immer in runde Klammern gesetzt: Rule? (induct “n“) Dieser Befehl besagt, dass im Folgenden das Prinzip der vollständigen Induktion auf die Variable n angewendet werden soll. Die Variable muss hierbei in Anführungsstriche gesetzt werden. Es ergeben sich dadurch zwei Unterziele oder auch „subgoals“. Das erste bezieht sich auf den Induktionsanfang, den Fall n = 0 : 21 this yields 2 subgoals closed_form.1 : |-----{1} sum(0) = (0*(0+1))/2 Rule? Zunächst muss hierbei die linke Seite, also sum(0), nach der oben festgelegten Definition ausformuliert werden. Dies geschieht durch den Befehl (expand “sum“): (IF 0=0 THEN 0 ELSE 0+sum(0-1) ENDIF) simplifies to 0 Expanding the definition of sum, this simplifies to: closed_form.1: |-----{1} 0 = 0 / 2 Rule? Die entstandene Gleichung kann durch einfache algebraische Umwandlung gelöst werden. PVS kann diesen Schritt selbst erledigen und der Benutzer muss lediglich (assert) eingeben. Nach dem Beweis des Induktionsanfangs kommt nun der Induktionsschritt als nächstes Unterziel: Simplifying, rewriting, and recording with decision procedures, This completes the proof of closed_form.1 closed_form.2: |-----{1} (FORALL(j: nat): sum(j) = (j*(j+1))/2 IMPLIES sum(j+1) = ((j+1)*(j+1+1)) /2) Rule? 22 Die Gleichung besagt: Es gilt: ∀j ∈ ℕ 0 : sum( j ) = ( j + 1)* ( j + 1 + 1) j *( j + 1) ⇒ sum( j + 1) = 2 2 ∀ := Menge aller Als erstes muss hierbei das „FORALL“ eliminiert werden. Dabei führt man durch den Befehl (skolem!) für alle Variablen neue Konstanten, also eine Art Platzhalter für konkrete Werte, ein. PVS vereinfacht zu: Skolemizing, this simplifies to: closed_form.2 |----{1} sum(j!1) = (j!1* (j!1+1))/2 IMPLIES sum(j!1+1) = ((j!1+1) * (j!1+1+1) / 2 Rule? Anschließend teilt man durch (flatten) die Formel auf zwei neue Formeln, ein „succedent“ und ein „andecedent“, auf. Dies entspricht dem Prinzip der vollständigen Induktion, denn diese verlangt im Induktionsschritt, mit Hilfe der Voraussetzung der Gültigkeit der Funktion für j!1, den Beweis ihrer Gültigkeit für j!1+1: Applying disjunctive simplification to flatten sequent, this simplifies to: closed_form.2: {-1} sum(j!1) = (j!1 * (j!1+1)) / 2 |----{1} sum(j!1+1) = ((j!1+1) * (j!1+1+1)) / 2 Als nächstes ist es wie auch beim Induktionsbeginn notwendig, „sum“ auszuformulieren. Der Befehl hierfür ist nun allerdings (expand “sum“+), da in diesem Fall nur die Ausformulierung im „succedent“ hilfreich ist: (IF j!1+1 = 0 THEN 0 ELSE j!1 + 1 + sum(j!1 + 1 – 1) ENDIF) simplifies to 1 + sum(j!1 ) + j!1 Expanding the definition of sum, this simplifies to: closed_form.2 : [-1] sum(j!1) = (j!1* (j!1+1)) / 2 |----{1} 1 + sum(j!1) + j!1 = (2 + j!1 + (j!1 * j!1 + 2 * j!1)) /2 23 Um den Beweis zu vervollständigen, muss zum Schluss auch hier eine einfache algebraische Umformung vorgenommen werden. Da sich hierbei im „succedent“ der gleiche Term wie im „andecedent“ ergibt und der „andecedent“ als gültig vorausgesetzt ist, muss auch der „succedent“ gelten. Der Befehl hierfür ist wieder (assert). Damit ist der Beweis abgeschlossen. PVS gibt folgende Informationen aus: Simplifying, rewriting, and recording with decision procedures, This completes the proof of closed_form.2. Q.E.D Runtime = 5.62 secs. Realtime =59.96 secs. Falls gewünscht, kann der Beweis gespeichert werden. 18 18 vgl. Sam Owre, Dr. John Rushby, Dr. Natarajan Shankar, Judy Crow, Mandayam Srivas: A Tutorial Introduction to PVS, II. Tutorial on Using PVS From Workshop on Industrial-Strength Formal Specification Techniques, ComputerScienceLaboratory SRI International, Boca Raton, Florida, April 1995 24 6 Insertsort 6.1 Strategie und funktionale Programmierung „Insertsort“ ist ein Programm, das eine Liste sortiert. Hierfür sind zwei Funktionen notwendig: „insert“ und „sortiere“. Außerdem werden noch einige Hilfsfunktionen und Hilfsschreibweisen verwendet: Im Folgenden sei: x, y ∈ ℕ 0 , l ∈ list[nat ] cdr (l ) := Ende der Liste l (die gesamte Liste, bis auf das erste Element ) car (l ) := erstes Element der Liste l y : l := y vorne an l angefügt , für l = [] [] „sortiere“ ist definiert als: sortiere(l ) = insert (car (l ), sortiere(cdr l )) , sonst Der Eingabewert ist also eine Liste der natürlichen Zahlen. Für die Eingabe einer leeren Liste ist der Ausgabewert wieder die leere Liste (Rekursionsanfang). Ansonsten wird die Funktion „insert“ aufgerufen. Die Eingabewerte sind das erste Element der Liste und erneut „sortiere“, mit dem Rest der Liste als Eingabewert (Rekursionsschritt). , für l = [] [ x] „insert“ ist definiert als: insert ( x, l ) = x : l , für x ≤ car l (car (l )) : insert ( x, cdr l ) , sonst l := sortierte Liste „insert“ ist eine Funktion mit einer Zahl und einer sortierten Int-Liste als Eingabewerte. Die Zahl wird nun größenmäßig in die Liste eingefügt und die neue Liste wird ausgegeben. Wenn die Liste leer ist, wird eine Liste mit der Zahl als Element ausgegeben. Ansonsten gibt es zwei Möglichkeiten: Entweder ist die Zahl kleiner als das erste Element der Liste, dann wird sie vorne angefügt. Oder sie ist größer, dann wird der Vorgang für die Zahl und den Rest der Liste wiederholt und das erste Element der Liste vorne angefügt. Ruft „sortiere“ die Funktion „insert“ auf, so wird der erste Wert der Eingabeliste in die restliche, sortierte Liste einsortiert. 25 Beispiel: sortiere([5, 4,3]) = insert (5, sortiere([4,3])) = insert (5, insert (4, sortiere([3]))) = insert (5, insert (4,[3])) = insert (5,[3, 4]) = [3, 4,5] Der Programmcode von Insertsort in Haskell lautet: insert :: Int->[Int]->[Int] --Name der Funktion; Einund Ausgabewert insert x [] = x:[] --Rekursionsanfang insert x l --Rekursionsschritt |(x<=(head l)) = x:l --Fallunterscheidung |otherwise = (head l):(insert x (tail l)) --Fallunterscheidung sortiere :: [Int]->[Int] --Name der Funktion; Einund Ausgabewert sortiere [] = [] --Rekursionsanfang sortiere (x:l) = insert x (sortiere l) --Rekursionsschritt 6.2 Beweise über Insertsort Um zu zeigen, dass „Insertsort“ seine Aufgabe erfüllt, sind folgende Beweise notwendig: (1) Beweis über den Erhalt der Länge nach dem Sortieren, um zu gewährleisten, dass keine Elemente zur Liste hinzukommen oder wegfallen. (2) Beweis über den Erhalt der Häufigkeit der einzelnen Elemente in der Liste nach dem Sortieren, um zu garantieren, dass keine Elemente durch andere ersetzt wurden. (3) Beweis, dass die Liste nach dem Sortiervorgang geordnet ist, um die Funktionsfähigkeit des Programms sicherzustellen. 26 6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen Nachdem das Programm in Haskell programmiert wurde, sollen nun die Beweise über seine Funktionsfähigkeit durchgeführt werden. Dies geschieht wieder in PVS. Als erstes müssen auch hier Theorie und Theoreme erstellt werden. Dazu sind allerdings einige Hilfsfunktionen notwendig: Sei : x ∈ ℕ 0 , l ∈ list[nat ] car (l ) := erstes Element der Liste; cdr (l ) := Re st der Liste cons ( x, l ) := x vorne an l angefügt Die Theorie mit den Theoremen lautet: insertsort: THEORY BEGIN insert(x:nat,l:list[nat]): RECURSIVE list[nat]= (IF null?(l) THEN (:x:) ELSIF x<=(car(l)) THEN cons(x,l) ELSE cons((car(l)),(insert(x,(cdr(l)))))ENDIF) MEASURE length(l) sortiere(l:list[nat]): RECURSIVE list[nat]= (IF null?(l) THEN null ELSE insert(car(l), (sortiere(cdr(l)))) ENDIF) MEASURE length(l) occ(x:nat, l:list[nat]): RECURSIVE nat= (IF null?(l) THEN 0 ELSIF x=car(l) THEN occ(x,cdr(l))+1 ELSE occ(x,cdr(l)) ENDIF) MEASURE length(l) sortiert(l:list[nat]): RECURSIVE boolean= (IF null?(l) THEN true ELSIF null?(cdr(l)) THEN true ELSIF car(l)<=car(cdr(l)) THEN true ELSE false ENDIF) MEASURE length(l) lemma_laengeliste: THEOREM FORALL(l:list[nat], x:nat): length(insert (x,l))=length(l)+1 laengeliste: THEOREM FORALL(l:list[nat]): length(sortiere(l))=length(l) lemma_erhaltelemente: THEOREM FORALL (x,y:nat, l:list[nat]): occ(x,insert(y,l))=(IF x=y THEN occ(x,l)+1 ELSE occ(x,l)ENDIF) erhaltelemente: THEOREM FORALL (x:nat, l:list[nat]): occ(x,l)=occ(x,sortiere(l)) lemma_sortierteliste: THEOREM FORALL (l:list[nat], x:nat): sortiert(l) IMPLIES sortiert(insert(x,l)) sortierteliste: THEOREM FORALL (l:list[nat]): sortiert(sortiere(l)) END insertsort19 19 vgl. Martin Hofmann: Vorlesungsskript Rechnergestütztes Beweisen, 5.6 First-order logic in PVS Wintersemester 2005/06 27 6.2.2 Beweis für den Erhalt der Länge einer Liste nach dem Sortiervorgang Für den Beweis sind zwei Theoreme der Theorie aus „6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen“ zu verifizieren. Die Funktion „length“ wird hier als Hilfsfunktion verwendet. Im Folgenden sei: x, y ∈ ℕ 0 ; l , k ∈ list[nat ] „length“ ist als Fallunterscheidung folgendermaßen definiert: , für l = [](leere Liste) 0 length(l ) = length(cdr (l )) + 1 , sonst In Worten ausgedrückt: length(l ) := Zahl der Elemente einer Liste Das Theorem „lemma_laengeliste“ wird als Hilfstheorem für den Beweis von „laengeliste“ verwendet. Der Beweis für dieses Theorem erfolgt durch vollständige Induktion. Lemma: Behauptung: length(insert ( x, l )) = length(l ) + 1 Beweis durch vollständige Induktion: (1) Induktionsanfang: Zu beweisen: length(insert ( x,[])) = length([]) + 1 ⇒ length([ x])) = 0 + 1 ⇒1=1 q.e.d. (2) Induktionsschritt: Zu beweisen: length(insert ( x, y : k )) = length( y : k ) + 1 , falls length(insert ( x, k )) = length(k ) + 1 Für x ≤ y : length(insert ( x, y : k )) = length( y : k ) + 1 ⇒ length( y : k ) + 1 = length( y : k ) + 1 q.e.d. 28 Für x > y : length(insert ( x, y : k )) = length( y : k ) + 1 ⇒ length( y : (insert ( x, k ))) = length( y : k ) + 1 ⇒ length(insert ( x, k )) + 1 = length(k ) + 2 ⇒ length(insert ( x, k )) = length(k ) + 1 Entspricht der Voraussetzung, die sich aus dem Induktionsschritt ergibt. q.e.d. Nun ist also nur noch das Theorem „laengeliste“ zu beweisen. Behauptung: length( sortiere(l )) = length(l ) Beweis durch vollständige Induktion: (1) Induktionsanfang: Zu beweisen: length( sortiere([])) = length([]) ⇒ length([]) = length([]) q.e.d. (2) Induktionsschritt: Zu beweisen: length( sortiere( y : l )) = length( y : l ) , falls length( sortiere(l )) = length(l ) length( sortiere( y : l )) = length( y : l ) ⇒ length(insert ( y, sortiere(l )) = length(l ) + 1 Mit Voraussetzung aus dem Induktionsschritt folgt: length(insert ( y, sortiere(l )) = length( sortiere(l )) + 1 Beweis durch Lemma q.e.d. Die Länge einer Liste bleibt also über den Sortiervorgang durch „Insertsort“ erhalten. Der Beweis in PVS ist in „7.3.1 Beweis für den Erhalt der Länge einer Liste nach dem Sortierverfahren“ zu finden. 29 6.2.3 Beweis für den Erhalt der Elemente einer Liste nach dem Sortierverfahren Für den Beweis des Erhaltes der Elemente wird die Funktion „occ“ benötigt, da so gezeigt werden kann, dass nach dem Sortierverfahren die Häufigkeit eines Elementes in der Liste gleich geblieben ist. Im Folgenden sei: x, y ∈ ℕ 0 ; l , k ∈ list[nat ] „occ“ ist als Fallunterscheidung folgendermaßen definiert: , für l = [](leere Liste) 0 occ( x, l ) = occ( x, cdr (l )) + 1 , für x = car (l ) occ( x, cdr (l )) , sonst In Worten ausgedrückt: occ( x, l ) := Häufigkeit des Vorkommens einer Zahl x in einer Liste l Für den Beweis des Erhaltes aller Listenelemente nach dem Sortiervorgang wird wie oben beschrieben ein Vorbeweis, ein so genanntes Lemma, benötigt. Lemma: Behauptung: (1) x = y : occ( x, insert ( y, l )) = occ( x, l ) + 1 (2) x ≠ y : occ( x, insert ( y, l )) = occ( x, l ) Auch diese Gleichungen sollen durch vollständige Induktion bewiesen werden: (1) Induktionsanfang: 1) x = y : Zu beweisen: occ( x, insert ( y, [])) = occ( x, []) + 1 ⇒ occ( x,[y])) = occ( x, []) + 1 ⇒ occ( x, []) + 1 = occ( x, []) + 1 q.e.d. 2) x ≠ y : Zu beweisen: occ( x, insert ( y, [])) = occ( x, []) ⇒ occ( x,[y])) = occ( x, []) ⇒ occ( x, []) = occ( x, []) q.e.d. 30 (2) Induktionsschritt: Sei: k ∧ y : k := sortierte Liste Zu beweisen: 1) x = y : occ( x, insert ( y, z : k )) = occ( x, z : k ) + 1 ,falls occ( x, insert ( y, k )) = occ( x, k ) + 1 2) x ≠ y : occ( x, insert ( y, z : k )) = occ( x, z : k ) , falls occ( x, insert ( y, k )) = occ( x, k ) Für x ≤ z : 1) Für x = y : occ( x, insert ( y, z : k )) = occ( x, z : k ) + 1 ⇒ occ( x, y : z : k ) = occ( x, z : k ) + 1 ⇒ occ( x, z : k ) + 1 = occ( x, z : k ) + 1 q.e.d. 2) Für x ≠ y : occ( x, insert ( y, z : k )) = occ( x, z : k ) Für y ≤ z folgt: occ( x, y : z : k ) = occ( x, z : k ) ⇒ occ( x, z : k ) = occ( x, z : k ) q.e.d. Für y > z folgt: occ( x, z : (insert ( y, k ))) = occ( x, z : k ) Für x = z folgt: occ( x, insert ( y, k )) + 1 = occ( x, k ) + 1 ⇒ occ( x, insert ( y, k )) = occ( x, k ) Entspricht der Induktionsschritt. q.e.d. 31 Voraussetzung durch den Für x < z folgt: occ( x, (insert ( y, k ))) = occ( x, k ) Mit der Voraussetzung aus dem Induktionsschritt folgt: occ( x, k ) = occ( x, k ) q.e.d. Für x > z : 1) Für x = y : occ( x, insert ( y, z : k )) = occ( x, z : k ) + 1 ⇒ occ( x, z : insert ( y, k )) = occ( x, k ) + 1 ⇒ occ( x, insert ( y, k )) = occ( x, k ) + 1 Mit der Voraussetzung aus dem Induktionsschritt folgt: occ( x, k ) + 1 = occ( x, k ) + 1 q.e.d. 2) Für x ≠ y folgt: Der Beweis für diesen Fall ist der Gleiche wie für den Fall x ≠ y , wenn x ≤ z . Hier entfällt nur der Fall x = z . Damit ist auch das gesamte Lemma bewiesen. Nun wird das Theorem „erhaltelemente“ verifiziert. Behauptung: occ( x, l ) = occ( x, sortiere(l )) Beweis: (1) Induktionsanfang: Zu beweisen: occ( x, []) = occ( x, sortiere([])) ⇒ occ( x, []) = occ( x, []) q.e.d. 32 (2) Induktionsschritt: Zu beweisen: occ( x, y : k ) = occ( x, sortiere( y : k )) , falls occ( x, k ) = occ( x, sortiere(k )) occ( x, y : k ) = occ( x, sortiere( y : k )) ⇒ occ( x, y : k ) = occ( x, insert ( y, sortiere(k ))) Für x = y : Mit Lemma folgt: occ( x, y : k ) = occ( x, sortiere(k )) + 1 Mit der Voraussetzung aus dem Induktionsschritt folgt: occ( x, y : k ) = occ( x, k ) + 1 ⇒ occ( x, k ) + 1 = occ( x, k ) + 1 q.e.d. Für x ≠ y : Mit Lemma folgt: occ( x, y : k ) = occ( x, sortiere(k )) Mit der Voraussetzung aus dem Induktionsschritt folgt: occ( x, y : k ) = occ( x, k ) ⇒ occ( x, k ) = occ( x, k ) q.e.d. Der Beweis ist abgeschlossen. Die Elemente einer Liste der natürlichen Zahlen bleiben also tatsächlich die gleichen, wenn diese durch „Insertsort“ sortiert wird. Die Beweise der Theoreme im Prover von PVS sind in „7.3.2 Beweis für den Erhalt der Elemente nach dem Sortierverfahren“ zu finden. 33 6.2.4 Beweis für die Sortierung einer Liste durch Insertsort Nun muss noch gezeigt werden, dass „Insertsort“ eine Liste auch sortiert. Auch hier ist allerdings erst einmal der Beweis eines Lemmas notwendig. Sei: x, y ∈ ℕ 0 ; l , k ∈ list[nat ] Lemma: Behauptung: insert ( x, l ) ⇒ sortierte Liste Beweis: (1) Induktionsanfang: Zu beweisen: insert ( x,[]) liefert sortierte Liste insert ( x,[]) = [ x] Liste mit einem Element ist immer sortiert q.e.d. (2) Induktionsschritt: k := sortierte Liste y : k := sortierte Liste ⇒ y ≤ car (k ) Zu beweisen: insert ( x, y : k ) liefert sortierte Liste, falls insert ( x, k ) liefert sortierte Liste Für x ≤ y : insert ( x, y : k ) = x : y : k x ist kleiner oder gleich dem kleinsten Element der sortierten Liste, die neue Liste ist also sortiert q.e.d. Für x > y : insert ( x, y : k ) = y : insert ( x, k ) y ist kleinstes Element der neuen Liste und insert ( x, k ) liefert nach Voraussetzung durch den Induktionsschritt eine sortierte Liste, die neue Liste ist also auch sortiert q.e.d. 34 Beweis für die Sortierung der Liste: Behauptung: sortiere(l ) sortiert die Liste Beweis: (1) Induktionsanfang: Zu beweisen: sortiere([]) bzw. sortiere([ x]) sortiert die Liste sortiere([]) = []; sortiere([ x]) = insert ( x, sortiere([])) = insert ( x,[]) = [ x] Leere Liste und Liste mit einem Element sind immer sortiert q.e.d. (2) Induktionsschritt: Zu beweisen: sortiere( y : k ) sortiert die Liste, falls sortiere(k ) die Liste sortiert sortiere( y : k ) = insert ( y, ( sortiere(k )) sortiere(k ) ist nach Voraussetzung durch den Induktionsschritt sortiert Beweis durch Lemma q.e.d. Der Beweis über die Funktionsfähigkeit des Programms Insertsort ist abgeschlossen, da alle seine Fälle bewiesen wurden. Der Beweis der Sortierung im Prover von PVS ist im Anhang zu finden. Dabei wird der Beweis über die Hilfsfunktion „sortiert“ durchgeführt, die eine Liste als Eingabewert hat und einen Wahrheitswert darüber ausgibt, ob die Liste sortiert ist. Damit wird das Theorem „lemma_sortierteliste“ und, mit diesem als Lemma, das Theorem „sortierteliste“ bewiesen (siehe auch „6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen“). Damit ist klar, dass eine Liste nach dem Sortieren durch „Insertsort“ geordnet ist. 35 7 Anhang 7.1 Datentypen und Operatoren20 Datentyp Bool Char String Int Float/Double Beschreibung Wahrheitswerte Beispiele/Schreibweise True, False Zeichen 'a' Liste von Zeichen/Zeichenketten "Hallo" bzw. ['H','a','l','l','o'] Ganze Zahlen von −2147483648 bis 2147483647 2 Gleitkommazahlen 2.312 Operator Test auf Gleichheit Test auf Ungleichheit ”kleiner als“ ”kleiner als oder gleich“ ”größer als“ ”größer als oder gleich“ Beispiele/Schreibweise == /= < <= > >= 7.2 Programmcode: Listenmaximum hlistenmaximum :: [Int] -> Int -> Int hlistenmaximum [] 0 = error "Leere Liste hat kein Maximum" hlistenmaximum [x] acc = acc hlistenmaximum (x:y:ys) acc |x>=y = hlistenmaximum (x:ys) x |x<y = hlistenmaximum (y:ys) y listenmaximum :: [Int] -> Int listenmaximum [x] = x listenmaximum liste = hlistenmaximum liste 0 20 Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, Kap.3.1 Elementare Datentypen, März 2003 36 7.3 Beweise in PVS 7.3.1 Beweis für den Erhalt der Länge einer Liste nach dem Sortierverfahren Als erstes wird das Theorem „lemma_laengeliste“ aus „6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen“ bewiesen. Danach wird das Theorem „laengeliste“ mit diesem Theorem als Lemma verifiziert: Starting pvs-cmulisp -qq ... CMU Common Lisp 19d (19D), running on Knoppix With core: /UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/pvs-cmulisp.core Dumped on: Wed, 2006-11-29 02:19:17+01:00 on photon.csl.sri.com See <http://www.cons.org/cmucl/> for support information. Loaded subsystems: Python 1.1, target Intel x86 CLOS based on Gerd's PCL 2004/04/14 03:32:47 ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/mu.so ... ;;; Done. ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/ws1s.so ... ;;; Done. ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/bdd-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/mu-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/dfa-foreigncmu.x86f". * * lemma_laengeliste : |------{1} FORALL (l: list[nat], x: nat): length(insert(x, l)) = length(l) + 1 Rerunning step: (INDUCT "l") Inducting on l on formula 1, this yields 2 subgoals: lemma_laengeliste.1 : |------{1} FORALL (x: nat): length(insert(x, null)) = length(null) + 1 Rerunning step: (EXPAND "insert") Expanding the definition of insert, this simplifies to: lemma_laengeliste.1 : |------{1} FORALL (x: nat): length((: x :)) = 1 + length(null) Rerunning step: (EXPAND "length") Expanding the definition of length, this simplifies to: lemma_laengeliste.1 : |------{1} FORALL (x: nat): length((: :)) = 0 Rerunning step: (EXPAND "length") Expanding the definition of length, 37 this simplifies to: lemma_laengeliste.1 : |------{1} TRUE which is trivially true. This completes the proof of lemma_laengeliste.1. lemma_laengeliste.2 : |------{1} FORALL (cons1_var: nat, cons2_var: list[nat]): (FORALL (x: nat): length(insert(x, cons2_var)) = length(cons2_var) + 1) IMPLIES (FORALL (x: nat): length(insert(x, cons(cons1_var, cons2_var))) = length(cons(cons1_var, cons2_var)) + 1) Rerunning step: (SKOLEM!) Skolemizing, this simplifies to: lemma_laengeliste.2 : |------{1} (FORALL (x: nat): length(insert(x, cons2_var!1)) = length(cons2_var!1) + 1) IMPLIES (FORALL (x: nat): length(insert(x, cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) + 1) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_laengeliste.2 : {-1} FORALL (x: nat): length(insert(x, cons2_var!1)) = length(cons2_var!1) + 1 |------{1} FORALL (x: nat): length(insert(x, cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) + 1 Rerunning step: (SKOLEM!) Skolemizing, this simplifies to: lemma_laengeliste.2 : [-1] FORALL (x: nat): length(insert(x, cons2_var!1)) = length(cons2_var!1) + 1 |------{1} length(insert(x!1, cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) + 1 Rerunning step: (INST?) Found substitution: x: nat gets x!1, Using template: x Instantiating quantified variables, this simplifies to: 38 lemma_laengeliste.2 : {-1} length(insert(x!1, cons2_var!1)) = length(cons2_var!1) + 1 |------[1] length(insert(x!1, cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) + 1 Rerunning step: (EXPAND "length" +) Expanding the definition of length, this simplifies to: lemma_laengeliste.2 : [-1] length(insert(x!1, cons2_var!1)) = length(cons2_var!1) + 1 |------{1} CASES insert(x!1, cons(cons1_var!1, cons2_var!1)) OF null: 0, cons(x, y): length(y) + 1 ENDCASES = 2 + length(cons2_var!1) Rerunning step: (GRIND) insert rewrites insert(x!1, cons(cons1_var!1, cons2_var!1)) to IF x!1 <= cons1_var!1 THEN cons(x!1, cons(cons1_var!1, cons2_var!1)) ELSE cons((car(cons(cons1_var!1, cons2_var!1))), (insert(x!1, (cdr(cons(cons1_var!1, cons2_var!1)))))) length rewrites length(cons(cons1_var!1, cons2_var!1)) to 1 + length(cons2_var!1) Trying repeated skolemization, instantiation, and if-lifting, This completes the proof of lemma_laengeliste.2. Q.E.D. Run time = 0.489 secs. Real time = 0.608 secs. NIL * laengeliste : |------{1} FORALL (l: list[nat]): length(sortiere(l)) = length(l) Rule? (induct "l") Inducting on l on formula 1, this yields 2 subgoals: laengeliste.1 : |------{1} length(sortiere(null)) = length(null) Rule? (expand "sortiere") Expanding the definition of sortiere, this simplifies to: laengeliste.1 : |------{1} TRUE which is trivially true. 39 This completes the proof of laengeliste.1. laengeliste.2 : |------{1} FORALL (cons1_var: nat, cons2_var: list[nat]): length(sortiere(cons2_var)) = length(cons2_var) IMPLIES length(sortiere(cons(cons1_var, cons2_var))) = length(cons(cons1_var, cons2_var)) Rule? (skolem!) Skolemizing, this simplifies to: laengeliste.2 : |------{1} length(sortiere(cons2_var!1)) = length(cons2_var!1) IMPLIES length(sortiere(cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) Rule? (flatten) Applying disjunctive simplification to flatten sequent, this simplifies to: laengeliste.2 : {-1} length(sortiere(cons2_var!1)) = length(cons2_var!1) |------{1} length(sortiere(cons(cons1_var!1, cons2_var!1))) = length(cons(cons1_var!1, cons2_var!1)) Rule? (grind) sortiere rewrites sortiere(cons(cons1_var!1, cons2_var!1)) to insert(cons1_var!1, (sortiere(cons2_var!1))) length rewrites length(cons(cons1_var!1, cons2_var!1)) to 1 + length(cons2_var!1) Trying repeated skolemization, instantiation, and if-lifting, this simplifies to: laengeliste.2 : [-1] length(sortiere(cons2_var!1)) = length(cons2_var!1) |------{1} length(insert(cons1_var!1, (sortiere(cons2_var!1)))) = 1 + length(cons2_var!1) Rule? (lemma "lemma_laengeliste") Applying lemma_laengeliste this simplifies to: laengeliste.2 : {-1} FORALL (l: list[nat], x: nat): length(insert(x, l)) = length(l) + 1 [-2] length(sortiere(cons2_var!1)) = length(cons2_var!1) |------[1] length(insert(cons1_var!1, (sortiere(cons2_var!1)))) = 1 + length(cons2_var!1) Rule? (inst?) Found substitution: l: list[nat] gets (sortiere(cons2_var!1)), x: nat gets cons1_var!1, Using template: length(insert(x, l)) Instantiating quantified variables, this simplifies to: 40 laengeliste.2 : {-1} length(insert(cons1_var!1, (sortiere(cons2_var!1)))) = length((sortiere(cons2_var!1))) + 1 [-2] length(sortiere(cons2_var!1)) = length(cons2_var!1) |------[1] length(insert(cons1_var!1, (sortiere(cons2_var!1)))) = 1 + length(cons2_var!1) Rule? (assert) Simplifying, rewriting, and recording with decision procedures, This completes the proof of laengeliste.2. Q.E.D. Run time = 0.35 secs. Real time = 29.123 secs. Renamed insertsort.prf to insertsort.prf~ Wrote proof file insertsort.prf NIL * 41 7.3.2 Beweis für den Erhalt der Elemente nach dem Sortierverfahren Als erstes wird das Theorem „lemma_erhaltelemente“ aus „6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen“ bewiesen. Danach wird das Theorem „erhaltelemente“ mit diesem Theorem als Lemma verifiziert: Starting pvs-cmulisp -qq ... CMU Common Lisp 19d (19D), running on Knoppix With core: /UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/pvs-cmulisp.core Dumped on: Wed, 2006-11-29 02:19:17+01:00 on photon.csl.sri.com See <http://www.cons.org/cmucl/> for support information. Loaded subsystems: Python 1.1, target Intel x86 CLOS based on Gerd's PCL 2004/04/14 03:32:47 ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/mu.so ... ;;; Done. ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/ws1s.so ... ;;; Done. ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/bdd-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/mu-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/dfa-foreigncmu.x86f". * * lemma_erhaltelemente : |------{1} FORALL (x, y: nat, l: list[nat]): occ(x, insert(y, l)) = (IF x = y THEN occ(x, l) + 1 ELSE occ(x, l) ENDIF) Rerunning step: (INDUCT "l") Inducting on l on formula 1, this yields 2 subgoals: lemma_erhaltelemente.1 : |------{1} FORALL (x, y: nat): occ(x, insert(y, null)) = (IF x = y THEN occ(x, null) + 1 ELSE occ(x, null) ENDIF) Rerunning step: (EXPAND "insert") Expanding the definition of insert, this simplifies to: lemma_erhaltelemente.1 : |------{1} FORALL (x, y: nat): occ(x, (: y :)) = (IF x = y THEN 1 + occ(x, null) ELSE occ(x, null) ENDIF) Rerunning step: (EXPAND "occ") Expanding the definition of occ, this simplifies to: lemma_erhaltelemente.1 : 42 |------{1} FORALL (x, y: nat): IF x = y THEN 1 + occ(x, (: :)) ELSE occ(x, (: :)) = (IF x = y THEN 1 ELSE 0 ENDIF) Rerunning step: (EXPAND "occ") Expanding the definition of occ, this simplifies to: lemma_erhaltelemente.1 : |------{1} TRUE which is trivially true. This completes the proof of lemma_erhaltelemente.1. lemma_erhaltelemente.2 : |------{1} FORALL (cons1_var: nat, cons2_var: list[nat]): (FORALL (x, y: nat): occ(x, insert(y, cons2_var)) = (IF x = y THEN occ(x, cons2_var) + 1 ELSE occ(x, cons2_var) ENDIF)) IMPLIES (FORALL (x, y: nat): occ(x, insert(y, cons(cons1_var, cons2_var))) = (IF x = y THEN occ(x, cons(cons1_var, cons2_var)) + 1 ELSE occ(x, cons(cons1_var, cons2_var)) ENDIF)) Rerunning step: (SKOLEM!) Skolemizing, this simplifies to: lemma_erhaltelemente.2 : |------{1} (FORALL (x, y: nat): occ(x, insert(y, cons2_var!1)) = (IF x = y THEN occ(x, cons2_var!1) + 1 ELSE occ(x, cons2_var!1) ENDIF)) IMPLIES (FORALL (x, y: nat): occ(x, insert(y, cons(cons1_var!1, cons2_var!1))) = (IF x = y THEN occ(x, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x, cons(cons1_var!1, cons2_var!1)) ENDIF)) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_erhaltelemente.2 : {-1} FORALL (x, y: nat): occ(x, insert(y, cons2_var!1)) = (IF x = y THEN occ(x, cons2_var!1) + 1 ELSE occ(x, cons2_var!1) ENDIF) |------{1} FORALL (x, y: nat): 43 occ(x, insert(y, cons(cons1_var!1, cons2_var!1))) = (IF x = y THEN occ(x, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x, cons(cons1_var!1, cons2_var!1)) ENDIF) Rerunning step: (SKOLEM!) Skolemizing, this simplifies to: lemma_erhaltelemente.2 : [-1] FORALL (x, y: nat): occ(x, insert(y, cons2_var!1)) = (IF x = y THEN occ(x, cons2_var!1) + 1 ELSE occ(x, cons2_var!1) ENDIF) |------{1} occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = (IF x!1 = y!1 THEN occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF) Rerunning step: (INST?) Found substitution: y: nat gets y!1, x: nat gets x!1, Using template: x = y Instantiating quantified variables, this simplifies to: lemma_erhaltelemente.2 : {-1} occ(x!1, insert(y!1, cons2_var!1)) = (IF x!1 = y!1 THEN occ(x!1, cons2_var!1) + 1 ELSE occ(x!1, cons2_var!1) ENDIF) |------[1] occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = (IF x!1 = y!1 THEN occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF) Rerunning step: (LIFT-IF) Lifting IF-conditions to the top level, this simplifies to: lemma_erhaltelemente.2 : {-1} IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) + 1 ELSE occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) ENDIF |------{1} IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF Rerunning step: (SPLIT) Splitting conjunctions, this yields 2 subgoals: lemma_erhaltelemente.2.1 : {-1} x!1 = y!1 AND 44 occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) + 1 |------[1] IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_erhaltelemente.2.1 : {-1} x!1 = y!1 {-2} occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) + 1 |------[1] IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF Rerunning step: (REPLACE -1 1) Replacing using formula -1, this simplifies to: lemma_erhaltelemente.2.1 : [-1] x!1 = y!1 [-2] occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) + 1 |------{1} occ(y!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(y!1, cons(cons1_var!1, cons2_var!1)) + 1 Rerunning step: (ASSERT) Simplifying, rewriting, and recording with decision procedures, this simplifies to: lemma_erhaltelemente.2.1 : [-1] x!1 = y!1 {-2} occ(x!1, insert(y!1, cons2_var!1)) = 1 + occ(x!1, cons2_var!1) |------{1} occ(y!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = 1 + occ(y!1, cons(cons1_var!1, cons2_var!1)) Rerunning step: (GRIND) insert rewrites insert(y!1, cons(cons1_var!1, cons2_var!1)) to IF y!1 <= cons1_var!1 THEN cons(y!1, cons(cons1_var!1, cons2_var!1)) ELSE cons((car(cons(cons1_var!1, cons2_var!1))), (insert(y!1, (cdr(cons(cons1_var!1, cons2_var!1)))))) occ rewrites occ(y!1, cons(cons1_var!1, cons2_var!1)) to IF y!1 = cons1_var!1 THEN occ(y!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 ELSE occ(y!1, cdr(cons(cons1_var!1, cons2_var!1))) occ rewrites occ(y!1, cons(y!1, cons(cons1_var!1, cons2_var!1))) to IF y!1 = cons1_var!1 THEN occ(y!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 ELSE occ(y!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 occ rewrites occ(y!1, cons(cons1_var!1, (insert(y!1, cons2_var!1)))) to IF y!1 = cons1_var!1 45 THEN occ(y!1, cdr(cons(cons1_var!1, (insert(y!1, cons2_var!1))))) + 1 ELSE occ(y!1, cdr(cons(cons1_var!1, (insert(y!1, cons2_var!1))))) Trying repeated skolemization, instantiation, and if-lifting, This completes the proof of lemma_erhaltelemente.2.1. lemma_erhaltelemente.2.2 : {-1} NOT x!1 = y!1 AND occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) |------[1] IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_erhaltelemente.2.2 : {-1} occ(x!1, insert(y!1, cons2_var!1)) = occ(x!1, cons2_var!1) |------{1} x!1 = y!1 [2] IF x!1 = y!1 THEN occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) + 1 ELSE occ(x!1, insert(y!1, cons(cons1_var!1, cons2_var!1))) = occ(x!1, cons(cons1_var!1, cons2_var!1)) ENDIF Rerunning step: (GRIND) insert rewrites insert(y!1, cons(cons1_var!1, cons2_var!1)) to IF y!1 <= cons1_var!1 THEN cons(y!1, cons(cons1_var!1, cons2_var!1)) ELSE cons((car(cons(cons1_var!1, cons2_var!1))), (insert(y!1, (cdr(cons(cons1_var!1, cons2_var!1)))))) occ rewrites occ(x!1, cons(cons1_var!1, cons2_var!1)) to IF x!1 = cons1_var!1 THEN occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 ELSE occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) occ rewrites occ(x!1, cons(y!1, cons(cons1_var!1, cons2_var!1))) to IF x!1 = cons1_var!1 THEN occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 ELSE occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) occ rewrites occ(x!1, cons(cons1_var!1, (insert(y!1, cons2_var!1)))) to IF x!1 = cons1_var!1 THEN occ(x!1, cdr(cons(cons1_var!1, (insert(y!1, cons2_var!1))))) + 1 ELSE occ(x!1, cdr(cons(cons1_var!1, (insert(y!1, cons2_var!1))))) Trying repeated skolemization, instantiation, and if-lifting, This completes the proof of lemma_erhaltelemente.2.2. This completes the proof of lemma_erhaltelemente.2. Q.E.D. 46 Run time = 0.583 secs. Real time = 0.975 secs. NIL * erhaltelemente : |------{1} FORALL (x: nat, l: list[nat]): occ(x, l) = occ(x, sortiere(l)) Rule? (induct "l") Inducting on l on formula 1, this yields 2 subgoals: erhaltelemente.1 : |------{1} FORALL (x: nat): occ(x, null) = occ(x, sortiere(null)) Rule? (expand "sortiere") Expanding the definition of sortiere, this simplifies to: erhaltelemente.1 : |------{1} TRUE which is trivially true. This completes the proof of erhaltelemente.1. erhaltelemente.2 : |------{1} FORALL (cons1_var: nat, cons2_var: list[nat]): (FORALL (x: nat): occ(x, cons2_var) = occ(x, sortiere(cons2_var))) IMPLIES (FORALL (x: nat): occ(x, cons(cons1_var, cons2_var)) = occ(x, sortiere(cons(cons1_var, cons2_var)))) Rule? (skolem!) Skolemizing, this simplifies to: erhaltelemente.2 : |------{1} (FORALL (x: nat): occ(x, cons2_var!1) = occ(x, sortiere(cons2_var!1))) IMPLIES (FORALL (x: nat): occ(x, cons(cons1_var!1, cons2_var!1)) = occ(x, sortiere(cons(cons1_var!1, cons2_var!1)))) Rule? (flatten) Applying disjunctive simplification to flatten sequent, this simplifies to: erhaltelemente.2 : {-1} FORALL (x: nat): occ(x, cons2_var!1) = occ(x, sortiere(cons2_var!1)) |------{1} FORALL (x: nat): 47 occ(x, cons(cons1_var!1, cons2_var!1)) = occ(x, sortiere(cons(cons1_var!1, cons2_var!1))) Rule? (skolem!) Skolemizing, this simplifies to: erhaltelemente.2 : [-1] FORALL (x: nat): occ(x, cons2_var!1) = occ(x, sortiere(cons2_var!1)) |------{1} occ(x!1, cons(cons1_var!1, cons2_var!1)) = occ(x!1, sortiere(cons(cons1_var!1, cons2_var!1))) Rule? (inst?) Found substitution: x: nat gets x!1, Using template: x Instantiating quantified variables, this simplifies to: erhaltelemente.2 : {-1} occ(x!1, cons2_var!1) = occ(x!1, sortiere(cons2_var!1)) |------[1] occ(x!1, cons(cons1_var!1, cons2_var!1)) = occ(x!1, sortiere(cons(cons1_var!1, cons2_var!1))) Rule? (grind) occ rewrites occ(x!1, cons(cons1_var!1, cons2_var!1)) to IF x!1 = cons1_var!1 THEN occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) + 1 ELSE occ(x!1, cdr(cons(cons1_var!1, cons2_var!1))) sortiere rewrites sortiere(cons(cons1_var!1, cons2_var!1)) to insert(cons1_var!1, (sortiere(cons2_var!1))) Trying repeated skolemization, instantiation, and if-lifting, this yields 2 subgoals: erhaltelemente.2.1 : {-1} occ(cons1_var!1, cons2_var!1) = occ(cons1_var!1, sortiere(cons2_var!1)) {-2} x!1 = cons1_var!1 |------{1} 1 + occ(cons1_var!1, sortiere(cons2_var!1)) = occ(cons1_var!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (lemma "lemma_erhaltelemente") Applying lemma_erhaltelemente this simplifies to: erhaltelemente.2.1 : {-1} FORALL (x, y: nat, l: list[nat]): occ(x, insert(y, l)) = (IF x = y THEN occ(x, l) + 1 ELSE occ(x, l) ENDIF) [-2] occ(cons1_var!1, cons2_var!1) = occ(cons1_var!1, sortiere(cons2_var!1)) [-3] x!1 = cons1_var!1 |------[1] 1 + occ(cons1_var!1, sortiere(cons2_var!1)) = occ(cons1_var!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (inst?) Found substitution: l: list[nat] gets (sortiere(cons2_var!1)), y: nat gets cons1_var!1, 48 x: nat gets cons1_var!1, Using template: occ(x, insert(y, l)) Instantiating quantified variables, this simplifies to: erhaltelemente.2.1 : {-1} occ(cons1_var!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) = occ(cons1_var!1, (sortiere(cons2_var!1))) + 1 [-2] occ(cons1_var!1, cons2_var!1) = occ(cons1_var!1, sortiere(cons2_var!1)) [-3] x!1 = cons1_var!1 |------[1] 1 + occ(cons1_var!1, sortiere(cons2_var!1)) = occ(cons1_var!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (assert) Simplifying, rewriting, and recording with decision procedures, This completes the proof of erhaltelemente.2.1. erhaltelemente.2.2 : [-1] occ(x!1, cons2_var!1) = occ(x!1, sortiere(cons2_var!1)) |------{1} x!1 = cons1_var!1 {2} occ(x!1, sortiere(cons2_var!1)) = occ(x!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (lemma "lemma_erhaltelemente") Applying lemma_erhaltelemente this simplifies to: erhaltelemente.2.2 : {-1} FORALL (x, y: nat, l: list[nat]): occ(x, insert(y, l)) = (IF x = y THEN occ(x, l) + 1 ELSE occ(x, l) ENDIF) [-2] occ(x!1, cons2_var!1) = occ(x!1, sortiere(cons2_var!1)) |------[1] x!1 = cons1_var!1 [2] occ(x!1, sortiere(cons2_var!1)) = occ(x!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (inst?) Found substitution: l: list[nat] gets (sortiere(cons2_var!1)), y: nat gets cons1_var!1, x: nat gets x!1, Using template: occ(x, insert(y, l)) Instantiating quantified variables, this simplifies to: erhaltelemente.2.2 : {-1} occ(x!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) = (IF x!1 = cons1_var!1 THEN occ(x!1, (sortiere(cons2_var!1))) + 1 ELSE occ(x!1, (sortiere(cons2_var!1))) ENDIF) [-2] occ(x!1, cons2_var!1) = occ(x!1, sortiere(cons2_var!1)) |------[1] x!1 = cons1_var!1 [2] occ(x!1, sortiere(cons2_var!1)) = occ(x!1, insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (assert) 49 Simplifying, rewriting, and recording with decision procedures, This completes the proof of erhaltelemente.2.2. This completes the proof of erhaltelemente.2. Q.E.D. Run time = 0.785 secs. Real time = 16.976 secs. Renamed insertsort.prf to insertsort.prf~ Wrote proof file insertsort.prf NIL * 50 7.3.3 Beweis für die Sortierung der Liste durch Insertsort Als erstes wird das Theorem „lemma_sortierteliste“ aus „6.2.1 Theorie und Theorem – Voraussetzungen und Behauptungen“ bewiesen. Danach wird das Theoem „sortierteliste“ mit diesem Theorem als Lemma verifiziert: Starting pvs-cmulisp -qq ... CMU Common Lisp 19d (19D), running on Knoppix With core: /UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/pvs-cmulisp.core Dumped on: Wed, 2006-11-29 02:19:17+01:00 on photon.csl.sri.com See <http://www.cons.org/cmucl/> for support information. Loaded subsystems: Python 1.1, target Intel x86 CLOS based on Gerd's PCL 2004/04/14 03:32:47 ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/mu.so ... ;;; Done. ;;; Opening as shared library /soft/PVS-4.0/bin/ix86-Linux/runtime/ws1s.so ... ;;; Done. ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/bdd-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/mu-cmu.x86f". ; Loading #P"/UNIONFS/soft/PVS-4.0/bin/ix86-Linux/runtime/dfa-foreigncmu.x86f". * * lemma_sortierteliste : |------{1} FORALL (l: list[nat], x: nat): sortiert(l) IMPLIES sortiert(insert(x, l)) Rerunning step: (SKOLEM!) Skolemizing, this simplifies to: lemma_sortierteliste : |------{1} sortiert(l!1) IMPLIES sortiert(insert(x!1, l!1)) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_sortierteliste : {-1} sortiert(l!1) |------{1} sortiert(insert(x!1, l!1)) Rerunning step: (EXPAND "insert" +) Expanding the definition of insert, this simplifies to: lemma_sortierteliste : [-1] sortiert(l!1) |------{1} IF null?(l!1) THEN sortiert((: x!1 :)) ELSE IF x!1 <= (car(l!1)) THEN sortiert(cons(x!1, l!1)) 51 ELSE sortiert(cons((car(l!1)), (insert(x!1, (cdr(l!1)))))) ENDIF ENDIF Rerunning step: (SPLIT) Splitting conjunctions, this yields 2 subgoals: lemma_sortierteliste.1 : [-1] sortiert(l!1) |------{1} null?(l!1) IMPLIES sortiert((: x!1 :)) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_sortierteliste.1 : {-1} null?(l!1) [-2] sortiert(l!1) |------{1} sortiert((: x!1 :)) Rerunning step: (EXPAND "sortiert") Expanding the definition of sortiert, this simplifies to: lemma_sortierteliste.1 : [-1] {-2} null?(l!1) (IF null?(l!1) THEN TRUE ELSIF null?(cdr(l!1)) THEN TRUE ELSIF car(l!1) <= car(cdr(l!1)) THEN TRUE ELSE FALSE ENDIF) |------{1} TRUE which is trivially true. This completes the proof of lemma_sortierteliste.1. lemma_sortierteliste.2 : [-1] sortiert(l!1) |------{1} NOT null?(l!1) IMPLIES IF x!1 <= (car(l!1)) THEN sortiert(cons(x!1, l!1)) ELSE sortiert(cons((car(l!1)), (insert(x!1, (cdr(l!1)))))) ENDIF Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_sortierteliste.2 : [-1] sortiert(l!1) |------{1} null?(l!1) {2} IF x!1 <= (car(l!1)) THEN sortiert(cons(x!1, l!1)) ELSE sortiert(cons((car(l!1)), (insert(x!1, (cdr(l!1)))))) ENDIF Rerunning step: (SPLIT) 52 Splitting conjunctions, this yields 2 subgoals: lemma_sortierteliste.2.1 : [-1] sortiert(l!1) |------{1} x!1 <= (car(l!1)) IMPLIES sortiert(cons(x!1, l!1)) [2] null?(l!1) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_sortierteliste.2.1 : {-1} x!1 <= (car(l!1)) [-2] sortiert(l!1) |------{1} sortiert(cons(x!1, l!1)) [2] null?(l!1) Rerunning step: (EXPAND "sortiert" +) Expanding the definition of sortiert, this simplifies to: lemma_sortierteliste.2.1 : [-1] x!1 <= (car(l!1)) [-2] sortiert(l!1) |------{1} IF null?(l!1) THEN TRUE ELSIF x!1 <= car(l!1) THEN TRUE ELSE FALSE [2] null?(l!1) Rerunning step: (ASSERT) Simplifying, rewriting, and recording with decision procedures, This completes the proof of lemma_sortierteliste.2.1. lemma_sortierteliste.2.2 : [-1] sortiert(l!1) |------{1} NOT x!1 <= (car(l!1)) IMPLIES sortiert(cons((car(l!1)), (insert(x!1, (cdr(l!1)))))) [2] null?(l!1) Rerunning step: (FLATTEN) Applying disjunctive simplification to flatten sequent, this simplifies to: lemma_sortierteliste.2.2 : [-1] sortiert(l!1) |------{1} x!1 <= (car(l!1)) {2} sortiert(cons((car(l!1)), (insert(x!1, (cdr(l!1)))))) [3] null?(l!1) Rerunning step: (EXPAND "insert") Expanding the definition of insert, this simplifies to: lemma_sortierteliste.2.2 : [-1] sortiert(l!1) |------[1] x!1 <= (car(l!1)) 53 {2} [3] sortiert(cons((car(l!1)), (IF null?((cdr(l!1))) THEN (: x!1 :) ELSIF x!1 <= (car((cdr(l!1)))) THEN cons(x!1, (cdr(l!1))) ELSE cons((car((cdr(l!1)))), (insert(x!1, (cdr((cdr(l!1))))))) ENDIF))) null?(l!1) Rerunning step: (GRIND) sortiert rewrites sortiert(l!1) to IF null?(cdr(l!1)) THEN TRUE ELSIF car(l!1) <= car(cdr(l!1)) THEN TRUE ELSE FALSE sortiert rewrites sortiert(cons((car(l!1)), (IF null?((cdr(l!1))) THEN (: x!1 :) ELSIF x!1 <= (car((cdr(l!1)))) THEN cons(x!1, (cdr(l!1))) ELSE cons((car((cdr(l!1)))), (insert(x!1, (cdr((cdr(l!1))))))) ENDIF))) to IF null?((IF null?((cdr(l!1))) THEN (: x!1 :) ELSIF x!1 <= (car((cdr(l!1)))) THEN cons(x!1, (cdr(l!1))) ELSE cons((car((cdr(l!1)))), (insert(x!1, (cdr((cdr(l!1))))))) ENDIF)) THEN TRUE ELSIF car(cons((car(l!1)), (IF null?((cdr(l!1))) THEN (: x!1 :) ELSIF x!1 <= (car((cdr(l!1)))) THEN cons(x!1, (cdr(l!1))) ELSE cons((car((cdr(l!1)))), (insert(x!1, (cdr((cdr(l!1))))))) ENDIF))) <= car(cdr(cons((car(l!1)), (IF null?((cdr(l!1))) THEN (: x!1 :) ELSIF x!1 <= (car((cdr(l!1)))) THEN cons(x!1, (cdr(l!1))) ELSE cons((car((cdr(l!1)))), (insert(x!1, (cdr((cdr(l!1))))))) ENDIF)))) THEN TRUE ELSE FALSE Trying repeated skolemization, instantiation, and if-lifting, This completes the proof of lemma_sortierteliste.2.2. This completes the proof of lemma_sortierteliste.2. Q.E.D. Run time = 0.325 secs. Real time = 0.521 secs. NIL * sortierteliste : 54 |------{1} FORALL (l: list[nat]): sortiert(sortiere(l)) Rule? (induct "l") Inducting on l on formula 1, this yields 2 subgoals: sortierteliste.1 : |------{1} sortiert(sortiere(null)) Rule? (expand "sortiere") Expanding the definition of sortiere, this simplifies to: sortierteliste.1 : |------{1} sortiert(null) Rule? (expand "sortiert") Expanding the definition of sortiert, this simplifies to: sortierteliste.1 : |------{1} TRUE which is trivially true. This completes the proof of sortierteliste.1. sortierteliste.2 : |------{1} FORALL (cons1_var: nat, cons2_var: list[nat]): sortiert(sortiere(cons2_var)) IMPLIES sortiert(sortiere(cons(cons1_var, cons2_var))) Rule? (skolem!) Skolemizing, this simplifies to: sortierteliste.2 : |------{1} sortiert(sortiere(cons2_var!1)) IMPLIES sortiert(sortiere(cons(cons1_var!1, cons2_var!1))) Rule? (flatten) Applying disjunctive simplification to flatten sequent, this simplifies to: sortierteliste.2 : {-1} sortiert(sortiere(cons2_var!1)) |------{1} sortiert(sortiere(cons(cons1_var!1, cons2_var!1))) Rule? (expand "sortiere"+) Expanding the definition of sortiere, this simplifies to: sortierteliste.2 : [-1] sortiert(sortiere(cons2_var!1)) 55 |------{1} sortiert(insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (lemma "lemma_sortierteliste") Applying lemma_sortierteliste this simplifies to: sortierteliste.2 : {-1} FORALL (l: list[nat], x: nat): sortiert(l) IMPLIES sortiert(insert(x, l)) [-2] sortiert(sortiere(cons2_var!1)) |------[1] sortiert(insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (inst?) Found substitution: l: list[nat] gets (sortiere(cons2_var!1)), x: nat gets cons1_var!1, Using template: sortiert(insert(x, l)) Instantiating quantified variables, this simplifies to: sortierteliste.2 : {-1} sortiert((sortiere(cons2_var!1))) IMPLIES sortiert(insert(cons1_var!1, (sortiere(cons2_var!1)))) [-2] sortiert(sortiere(cons2_var!1)) |------[1] sortiert(insert(cons1_var!1, (sortiere(cons2_var!1)))) Rule? (assert) Simplifying, rewriting, and recording with decision procedures, This completes the proof of sortierteliste.2. Q.E.D. Run time = 0.22 secs. Real time = 15.963 secs. Renamed insertsort.prf to insertsort.prf~ Wrote proof file insertsort.prf NIL * 56 8 Zusammenfassung Im Rahmen dieser Seminararbeit habe ich mich seit fast einem Jahr mit dem Thema Rekursion beschäftigt. Dabei ist mir vor allem die extreme Erleichterung von Funktionsdefinitionen aufgefallen. Auch die Arbeit mit der Beweistechnik der vollständigen Induktion führte mir vor Augen, wie gravierend ein Beweis durch die richtige Vorgehensweise vereinfacht werden kann. Mit der funktionalen Programmierung habe ich mehr oder weniger meine ersten Erfahrungen im Programmieren gesammelt. Dies fiel mir durch die Anwendbarkeit des rekursiven Prinzips in diesem Bereich deutlich leichter und brachte mir das Programmieren näher. Die Arbeit mit PVS forderte eine längere Einarbeitungszeit. Dieser Schritt brachte mir allerdings auch für die übrigen Bereiche der Arbeit viel, da ich durch die Beweise mit dem Programm gelernt habe, allgemein korrekte Beweise anzufertigen und genauer zu notieren. Die Seminararbeit ist in Zusammenarbeit mit der LMU München entstanden. Professor Martin Hofmann sorgte für die nötige Einführung in das Thema und stand für Fragen jeder Art zur Verfügung. Außerdem ist die Arbeit Teil eines Schulprojektes am Gymnasium Ottobrunn, geleitet von StR Peter Brichzin. Die Teilnehmer erhielten hierbei die Möglichkeit einen Informatik Grundkurs zu besuchen und wurden nebenbei im Präsentieren geschult, da der Fortschritt der Arbeit in Zwischenberichten vor den übrigen Grundkursmitgliedern dargeboten wurde. Ich möchte den beiden an dieser Stelle für ihre tatkräftige Unterstützung danken, die es mir ermöglichte, eine für mich interessante Facharbeit anzufertigen und nebenbei einen Einblick in ein außerschulisches Thema zu bekommen. 57 9 Literaturverzeichnis Titelbild: - StR Peter Brichzin: Informatik am Gymnasium–Nachqualifikation von Lehrkräften (SIGNAL) - Rekursives Problemlösen, 1. Rekursion und Iteration, Einführungskurs für Nichtmathematiker, Akademie für Lehrerfortbildung und Personalführung, Dillingen, 13.9/14.9.2004 Bücher: - Nicolas Bourbaki: Elements de Mathematique - Algebre Commutative, AddisonWesley Publishing Company, Paris 1972 - von Meyers Lexikonredaktion: Duden Informatik-Fachlexikon für Studium und Praxis, Dudenverlag, 3.Auflage, Mannheim 2001 - von Meyers Lexikonredaktion: Duden Informatik Lehrbuch S II – Gymnasiale Oberstufe, Duden Paetec Schulbuchverlag - Otto Forster: Analysis 1-Differential- und Integralrechnung einer Veränderlichen, Friedrich Vieweg & Sohn Verlag, 7.Auflage, Wiesbaden 2004 Skripte: - StR Peter Brichzin: Informatik am Gymnasium–Nachqualifikation von Lehrkräften (SIGNAL) - Rekursives Problemlösen, Einführungskurs für Nichtmathematiker, Akademie für Lehrerfortbildung und Personalführung, Dillingen, 13.9/14.9.2004 - Dr. Hermann Puhlmann: Funktionale Programmierung mit Haskell, März 2003 - Sam Owre, Dr. John Rushby, Dr. Natarajan Shankar, Judy Crow, Mandayam Srivas: A Tutorial Introduction to PVS, From Workshop on Industrial-Strength Formal Specification Techniques, ComputerScienceLaboratory SRI International, Boca Raton, Florida, April 1995 - Martin Hofmann: Vorlesungsskript Rechnergestütztes Beweisen, Wintersemester 2005/06 Internet: - http://de.wikipedia.org/wiki/Formale_Sprache, http://de.wikipedia.org/wiki/LambdaKalk%C3%BCl#Einf.C3.BChrung, 24.03.07 58 10 Erklärung Ich erkläre hiermit, dass ich die Seminararbeit ohne fremde Hilfe angefertigt und nur die im Literaturverzeichnis angeführten Quellen und Hilfsmittel verwendet habe. …………………………,den………………… Ort Datum …..…………………………… Unterschrift des Verfassers Ende des Dokumentes 59