Datenstrukturen, Algorithmen und Programmierung 1 Vorlesungsskriptum Wintersemester 2006/2007 Prof. Dr. Bernhard Steffen Prof. Dr. Markus Müller-Olm Dr. Oliver Rüthing Fachbereich Informatik Universität Dortmund 2005 Inhaltsverzeichnis 1 Einführung 1.1 1.2 1.3 2 Was ist Informatik? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.1.1 Geschichtliches zur Computer–Entwicklung . . . . . . . . . . . . . . 5 1.1.2 Der Algorithmenbegriff . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.1.3 Problem, Algorithmus, Programm . . . . . . . . . . . . . . . . . . . 8 Information und Repräsentation . . . . . . . . . . . . . . . . . . . . . . . . 14 1.2.1 Semantikschemata . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 1.2.2 Nachrichtenverarbeitung . . . . . . . . . . . . . . . . . . . . . . . . . 20 1.2.3 Kodierung und Verschlüsselung . . . . . . . . . . . . . . . . . . . . . 22 Struktur und Invarianz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 1.3.1 Induktives Definieren . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 1.3.2 Induktives Beweisen . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 1.3.3 Anwendung: Semantik Boolescher Terme . . . . . . . . . . . . . . . . 34 2 WHILE–Programme und ihre Semantik 44 2.1 Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 2.2 Syntax von WHILE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 2.3 Semantik von WHILE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48 2.3.1 Semantik von arithmetischen und Booleschen Ausdrücken . . . . . . 48 i INHALTSVERZEICHNIS 1 2.3.2 Operationelle Semantik . . . . . . . . . . . . . . . . . . . . . . . . . 54 2.3.3 Denotationelle Semantik . . . . . . . . . . . . . . . . . . . . . . . . . 61 2.3.4 Axiomatische Semantik . . . . . . . . . . . . . . . . . . . . . . . . . 81 Kapitel 1 Einführung 1.1 Was ist Informatik? Informatik ist die Wissenschaft von der Informationsverarbeitung.“ ” Auffallend ist, daß in dieser Definition das Wort Computer nicht vorkommt. Besser als in der englischen Bezeichnung “Computer Science” wird hier der allgemeine Charakter des Fachgebietes klar: Im Mittelpunkt der Informatik steht nicht der Umgang mit dem Computer, sondern allgemeine Prinzipien und Vorgehensweisen zur Beherrschung komplexer Vorgänge und Systeme. Auf die Programmierung bezogen bedeutet das, daß weniger das eigentliche Programmierhandwerk und die Fertigkeit der Rechnerbenutzung im Vordergrund stehen, sondern eher prinzipielle Fragen wie: • wo liegen die Grenzen des (automatisch) Berechenbaren? Tatsächlich lassen sich einige Probleme gar nicht mit Hilfe eines automatischen Verfahrens lösen, andere wieder erfordern einen sehr hohen Mindestaufwand (eine sehr hohe Komplexität) bei der Berechnung. • wie muß man vorgehen, um auch sehr komplexe Probleme zuverlässig behandeln zu können? Die Gebiete Software–Engineering und Software–Projektmanagement beschäftigen sich mit der Organisation umfangreicher Programmpakete und das Gebiet Datenbanken sowie Teilgebiete der K ünstlichen Intelligenz mit der Organisation großer Datenmengen. • warum ist eine Programmier–, Modellierungs– oder Spezifikationssprache so und nicht anders entworfen worden (formales Sprachdesign)? • welche Maschinenarchitekturen sind zur Realisierung spezieller Problemklassen am besten geeignet? Hier werden z.B. abstrakte Maschinen– und Berechnungs2 1.1. WAS IST INFORMATIK? 3 modelle studiert, um deren Anwendungsprofile zu ermitteln. Besonders interessant ist hier die Untersuchung verteilter Systeme und paralleler Algorithmen. • was sind die wesentlichen Charakteristika eines Problems im Hinblick auf eine abstrakte Modellierung (Problemanalyse, Requirements Engineering)? • wann läßt sich ein gegebenes Verfahren einsetzen (Anwendungsprofile)? • wer sind die zukünftigen Interessenten und Anwender (Relevanzanalyse)? Bemerkung: Fragenstellen besitzt einen sehr hohen Stellenwert in der Wissenschaft allgemein, nicht nur in der Informatik. Oft liegt das zentrale Problem mehr in der richtigen Fragestellung, als in der späteren Beantwortung. Typisch für die Informatik ist also die Einbeziehung der Meta–Ebene“. Weniger das ” spezielle Anwendungsprogramm ist Untersuchungsgegenstand, sondern das Design des Gesamtkontextes steht im Vordergrund, in dem ein Problem behandelt oder Rechner und Software entwickelt werden sollen. Der Informatiker schafft sich gewissermaßen die Welt selbst, in der er arbeitet. Natürlich werden auch auf dieser Meta–Ebene Rechner zur Unterstützung eingesetzt, sowohl zur Analyse der derzeitigen ‘Programmierwelt’, als auch als deren Bestandteile. Stichworte sind hier CASE (= Computer Aided Software– Engineering) und CAD (= Computer Aided Design). In speziellen Bereichen gibt es bereits Programmgeneratoren, die aus formalen Beschreibungen automatisch lauffähigen Programmcode erzeugen. Prominentes Beispiel hierfür sind die sogenannten Compiler– Compiler, die eine formale Programmiersprachenbeschreibung in einen Compiler umsetzen. Grundlagen dieses Vorgehens sind ursprünglich hauptsächlich in der Mathematik beheimatete Fachgebiete wie: Algebra, Logik, Statistik, Zahlentheorie, Operations Research, Informationstheorie, Codierungstheorie, Modelltheorie, Kryptographie, Topologie, Geometrie, Kontrolltheorie, Kybernetik, die inzwischen ihrerseits stark von der Informatik geprägt wurden. Jedoch anders als z.B. in der Mathematik, wo vergleichsweise sparsame Formalismen verwendet werden, ist für die Informatik typisch, daß die betrachteten Strukturen oder Objekte (Formeln, Programme, Datenstrukturen, ...) konstruktiv aus recht umfangreichen Kollektionen von Grundobjekten mit Hilfe zahlreicher Operatoren gebildet werden. Man vergleiche z.B. die Struktur einer Gruppe oder eines Vektorraumes mit der einer Programmiersprache. Konsequenz dieses strukturellen Reichtums ist die zum Teil m ühsame und im Vergleich zur Mathematik unelegante Beweisführung bei der Analyse solcher Strukturen. Typisch sind die konstruktive Struktur ausnutzende Induktionsbeweise, die zum Teil ermüdende Fallunterscheidungen verlangen (strukturelle Induktion). Aber auch hier kann die geeignete Strukturierung der Beweise selbst Erstaunliches bewirken. 4 KAPITEL 1. EINFÜHRUNG Aber nicht nur das technische Vorgehen unterscheidet sich. Auch die grundsätzlichen Fragestellungen sind wegen des ‘konstruktiven’ Ansatzes der Informatik unterschiedlich. Während der Mathematiker mit der Existenz und ggf. Eindeutigkeit gewisser Objekte zufrieden sein mag, ist der Informatiker erst glücklich, wenn er das gewünschte Objekt effektiv konstruieren kann. Neben der Komplexität der betrachteten Strukturen ist vor allem auch die Flexibilität, mit der die betrachteten Objekte modifiziert oder ausgetauscht werden, für die Informatik charakteristisch. Die rasche Entwicklung neuer Technologien und Konzepte und die dadurch auftretenden neuen Probleme verlangen die Bereitschaft zu schnellem Umdenken. Der oben erwähnte Aufbau des Gesamtkontextes muß ständig neu überdacht, ausgebessert oder gar erneuert werden. Damit finden sich in der Informatik Charakteristika verschiedener klassischer Disziplinen: • der pragmatische, ja zum Teil experimentelle Ansatz beim Design des Kontextes hat ingenieurwissenschaftlichen Charakter, • die formale Analyse der (komplexen) Strukturen und Phänomene ist mathematisch und • die Betrachtung von Anwenderakzeptanz sowie die wissenschaftliche Auseinandersetzung mit Sprachstrukturen sind geisteswissenschaftlicher Natur. Gemeinsam ist diesen Punkten die Auseinandersetzung mit abstrakten Strukturen (Programmstrukturen, Sprachstrukturen, Datenstrukturen, Rechnerstrukturen, ...), die in der reinen Form weder geisteswissenschaftlich, noch naturwissenschaftlich oder ingenieurwissenschaftlich ist. Deshalb bezeichnet man die Informatik, wie auch die Mathematik, als eine Strukturwissenschaft. Die Informatik selbst wird üblicherweise in drei Bereiche eingeteilt (siehe Abbildung 1.1). Informatik KernInformatik Praktische Informatik Technische Informatik Abbildung 1.1: Die drei Bereiche der Informatik 1.1. WAS IST INFORMATIK? 1.1.1 5 Geschichtliches zur Computer–Entwicklung zw. 1623 und 1818 erste mechanische Rechenmaschinen (u.a. von Schickard, Blaise Pascal, Gottfried W. Leibnitz, P. M. Hahn, C. Thomas) um 1800 J. Jacquard entwickelt einen automatischen Webstuhl, der mittels Lochkarten gesteuert wird. 1833 Charles Babbage plant einen programmgesteuerten Rechner. 1886 H. Hollerith entwickelt verschiedene elektrische Lochkartenmaschinen, z.B. Sortierer, die u.a. für die Volkszählung eingesetzt wurden. Ihre Programmierung“ erfolgte durch ” Stecktafeln. 1934,41 Konrad Zuse plant und baut die erste funktionsfähige, programmierbare Rechenmaschine (die Z3). Diese wurde mittels Lochstreifen programmiert (die bei Bedarf zu Schleifen zusammengeklebt werden konnten). Multiplikationszeit: ca. 3 Sekunden. 1944 H. Aiken (Harvard–Universität, in Zusammenarbeit mit IBM) baut die MARK 1. 1946 die erste Rechenmaschine mit Elektronenröhren (die ENIAC). 18000 Elektronenröhren. Multiplikationszeit: 3 ms. Danach beginnt eine stürmische Entwicklung, die man heutzutage in Generationen“ un” terteilt: 1. Elektronenröhren dienen als Schaltelemente. 2. Generation (ab 1957 bis Ende der 60er Jahre) Transistoren als Schaltelemente. 3. Generation (ab Mitte der 60er Jahre) Mikromodultechnik mit ca. 20 Schaltelementen pro cm2 . 4. Generation (ab Anfang der 70er Jahre) hochintegrierte Schaltkreise. 5. Generation (seit Anfang der 80er Jahre) höchstintegrierte Schaltkreise (VLSI); mehrere Prozessoren auf einem Chip. 6. Generation (90er Jahre) hochprogrammierbare Bausteine (FPGAs, Field Programmable Gate Arrays), Embedded Systems. 7. Generation (seit 2000) SoCs (Systems on a Chip), MEMSs (Micro-electro-mechanical systems). Parallel zu dieser technologischen Entwicklung verläuft zwangsläufig auch eine Entwicklung der Software (wobei dieser Aspekt immer mehr Bedeutung einnimmt). 6 KAPITEL 1. EINFÜHRUNG 1. Generation Programmierung in Maschinencode. 2. Generation Assemblersprachen und Entwicklung erster höherer“ Programmier” sprachen wie FORTRAN, ALGOL, COBOL, LISP. 3. Generation Entwicklung von Betriebssystemen mit Dialogbetrieb, Datenbanken, Methoden der strukturierten Programmierung, Programmiersprachen C, PASCAL, MODULA. 4. Generation Verteilte Systeme, Rechnernetze, Kommunikationsfähigkeit, hochwertige Programmierumgebungen. Programmiersprachen Smalltalk, C++, Oberon, ADA. 5. Generation Wissensverarbeitung, automatisches Schlußfolgern, deduktive Datenbanken, Expertensysteme, Programmierspache PROLOG. 6. Generation Ereignisorientierte Programmierung, Internetprogrammierung, Integrationsframeworks, Middleware, Programmierspachen JAVA, Delphi, Erlang. Generelle Tendenz Bei einzelnen Systemen verschwinden die Unterschiede zwischen mechanischen und elektronischen Systemen (MEMSs), und ebenso zwischen Hardware und Software (HardwareSoftware Codesign, SoCs) zunehmend. Die Heterogenität der Systeme (Ubiquitous Computing, Networks on a Chip) ist ein prägendes Merkmal. Umfangreiche Netzwerke von Systemen und Endgeräten (devices) sind zu beherrschen und zu programmieren (JINI Projekt). 1.1.2 Der Algorithmenbegriff Ein zentraler Begriff in der Informatik ist der des Algorithmus. Ein Algorithmus ist eine Vorschrift, eine Anleitung zur Lösung einer Aufgabenstellung, die so präzise formuliert ist (z.B. in einer formalisierten (Programmier–) Sprache), daß sie im Prinzip mechanisch“ (per Computer) ausgeführt werden kann. Der Algorithmenbegriff ” ist also unabhängig von der Existenz einer speziellen Programmiersprache (in der der Algorithmus codiert sein kann) und einer speziellen Maschine (auf der der Algorithmus ausgeführt werden kann). Im Alltag begegnen einem viele Algorithmen (Kochrezepte, Bedienungsanleitungen, die Schulmethode zur Multiplikation zweier Zahlen, etc. ). Da Algorithmen abstrakte Konzepte sind, die unabhängig von den technologischen Möglichkeiten existieren, ist es nicht verwunderlich, daß es Algorithmen schon im Altertum 1.1. WAS IST INFORMATIK? 7 gab. Bereits Euklids Buch Elemente“ (3. Jahrhundert v. Chr.) kann als eine Sammlung ” von Algorithmen aufgefaßt werden. Der sog. Euklidsche Algorithmus wird heute noch in der Computer–Zahlentheorie und Kryptographie verwendet. Er ist nach wie vor der effizienteste Algorithmus zur Bestimmung des größten gemeinsamen Teilers zweier Zahlen (siehe späterer Abschnitt). Das Wort Algorithmus ist übrigens von dem Namen des Mathematikers Ibn Musa Al–Khowarizmi abgeleitet, der um 800 in Bagdad bei dem Kalifen Harun Al Raschid wirkte und verschiedene Bücher über die algorithmische“ Behandlung algebraischer“ ” ” Gleichungen schrieb. Erst seit ca. 1930 (also vor der Computerzeit) gelang es, eine formal exakte Theorie der Algorithmen und der algorithmischen Berechenbarkeit aufzustellen. In diesem Zusammenhang stellte sich überraschenderweise heraus, daß gewisse Probleme prinzipiell nicht algorithmisierbar sind. Insbesondere war es der G ödelsche Unvollständigkeitssatz, der damals großes Aufsehen erregte (auch heute noch: vgl. das Buch Gödel, Escher, Bach“ ” von D. Hofstadter). Der Satz besagt (unter anderem), daß die Arithmetik, also die elementare Zahlentheorie, nicht algorithmisierbar ist. Das bedeutet, daß wir nur in Einzelfällen die Richtigkeit oder Falschheit von arithmetischen Formeln, wie etwa ∀ x ∀ y. ((x + y) ∗ (x − y) = x ∗ x − y ∗ y), bestätigen können. Es ist aber prinzipiell unmöglich, hierfür ein universelles Verfahren anzugeben. Solch ein Verfahren würde für jede beliebige Formel nach endlicher Zeit entscheiden, ob diese Formel gültig ist oder nicht. Als Urväter“ der Berechenbarkeitstheorie, und damit der Grundlagen der theoretischen ” Informatik, wären zu nennen: K. Gödel, A. Church, S. Kleene, E. Post und insbesondere A. M. Turing, dessen theoretische Arbeiten bis heute große Bedeutung haben (insbesondere seine Arbeit: On computable numbers, with an application to the Entscheidungs” problem“). Flußdiagramme Eine Methode zur semi-formalen Darstellung von Algorithmen sind Flußdiagramme, deren Syntax in Abbildung 1.2 beschrieben ist. Bemerkung 1.1 Flußdiagramme sind unstrukturiert: Teilstrukturen wie WHILE–Schleifen sind dort nicht explizit vorhanden, sondern müssen durch GOTO“-Anweisungen modelliert wer” den. Das widerspricht der modernen Auffassung über Strukturiertheit von Programmen, und reduziert die Anwendbarkeit der Flußdiagramme auf kleine Algorithmen. Um den Vorteil graphischer Formalismen auch für größere Algorithmen anwenden zu können, muß 8 KAPITEL 1. EINFÜHRUNG ja Start Stop z=0 Wende Formel (3) an a==b ? nein a<b Vergleiche a und b Einstiegs− Ausstiegsstelle elementare Anweisung a>b Verzweigung aufgrund einer Bedingung a=b Lies x ein Gib x aus Eingabe, Ausgabe Abbildung 1.2: Flußdiagramme Struktur, z.B. ein Hierarchiekonzept eingeführt werden. Solche Konzepte werden im Rahmen des Software–Engineering studiert. Beispiele hierfür sind Struktogramme und SADT– Diagramme. 3 1.1.3 Problem, Algorithmus, Programm Beim Übergang von einem (evtl. umgangssprachlich formulierten) Problem, über einen abstrakten Algorithmus, der das Problem lösen soll, bis zu einem Programm, welches dann auf einer Maschine ablaufen soll, müssen mehrere Phasen (oft auch wiederholt) durchlaufen werden (vgl. auch Abbildung 1.3): Vorgehen bei der Algorithmenentwicklung 1. Problemformulierung 2. Problemanalyse, Problemabstraktion und –formalisierung, Problemspezifikation 9 1.1. WAS IST INFORMATIK? reale Welt Problem Abstraktion abstrakte Objekte Problemanalyse, funktionale Spezifikation, Algorithmenentwurf Algorithmus Darstellung informationsverarbeitendes System Darstellung in formalisierter Sprache zur Ausführung durch Rechner Programm Realisierung Umsetzung in Elementarschritte einer Maschine Maschine Abbildung 1.3: Phasen der Programmentwicklung 3. Algorithmenentwurf 4. Korrektheitsnachweis, Verifikation 5. Aufwandsanalyse (Komplexitätsanalyse) 6. Programmkonstruktion Wir führen diese Prinzipien vollständig an einem einfachen Beispiel vor. Hierbei soll bewußt in Kauf genommen werden, daß einiges an Stoff vorweggenommen werden muß. Es sollen einfach die unterschiedlichen Facetten der Informatik, von Theorie bis Praxis, aufgezeigt werden. Beispiel: 1. Problemformulierung Suche in einer aufsteigend sortierten Liste mit n natürlichen Zahlen die Zahl x. 2. (a) Problemanalyse Zu untersuchende Aspekte sind hier z.B.: • Gibt es überhaupt eine (eindeutige) Lösung? • Falls ja, wie kann die Lösung aussehen? • Hat das Problem eine inhärente Struktur? Kann man es in (einfachere) Teilprobleme zerlegen? 10 KAPITEL 1. EINFÜHRUNG (b) Problemabstraktion und –formalisierung Was ist Liste a mit n natürlichen Zahlen“? ” =⇒ n–stelliges Tupel mit Zahlen a1 ,a2 ,...,an , d.h.: a =df (a1 , a2 , ..., an ). Was heißt aufsteigend sortiert“? ” =⇒ a1 ≤ a2 ≤ · · · ≤ an Was heißt suche“? ” =⇒ Überprüfen der Existenz eines Index m mit a m = x. (c) Problemspezifikation Eingabe: Liste natürlicher Zahlen a = (a1 , a2 , ..., an ) mit a1 ≤ a2 ≤ · · · ≤ an und eine zu suchende Zahl x. Ausgabe: Antwort zu der Frage, ob die Zahl x in a vorkommt. Falls die Antwort ja“ lautet, soll zusätzlich ein Index m mit am = x ausgegeben werden ” (oft modifiziert man die Problemspezifikation derart, daß die Menge aller oder bestimmter, z.B. der kleinste, Indizes m mit a m = x ausgegeben werden soll). 3. Algorithmenentwurf Dies entspricht der Ableitung eines Verfahrens aus der Problemspezifikation: Oft gibt es verschiedene Alternativen. Erwünscht sind elegante, kurze, verständliche, transparente, schnelle, leicht veränderbare Lösungen, wobei in der Regel nicht alle Eigenschaften miteinander vereinbar sind. Eine der möglichen Alternativen ist unter der Bezeichnung Bin äres Suchen bekannt. Wir wollen zunächst diese Lösung informell mit Hilfe eines Flußdiagramms skizzieren (vgl. Abbildung 1.4). Die Grundidee ist, ein beliebiges Element, zum Beispiel a m , auszuwählen und mit dem Suchargument x zu vergleichen. Ist es gleich x, so ist die Suche beendet. Ist es kleiner als x, so folgern wir aus der Sortiertheit der Liste, daß alle Elemente mit kleinerem Index ausgeschlossen werden können. Ist es aber größer als x, so sind alle Elemente mit größerem Index auszuschließen. Diese Methode heißt Bin äres Suchen, weil in jedem Schritt entweder die Elemente oberhalb oder unterhalb der gewählten Stichprobe eliminiert werden können. Wie im Punkt “Aufwandsanalyse” auf Seite 12 später ausgeführt wird, empfiehlt es sich aus Effizienzgründen dabei das mittlere Element zu wählen. Anders als in der informellen Algorithmenbeschreibung unten ist diese Wahl bereits in die Flussdiagrammdarstellung integriert. Diese Formulierung suggeriert, welche Objekte (besser: Datenstrukturen) für die Formalisierung benötigt werden. Beispiel: Wir verwenden zwei Indexvariablen L und R, die das linke und das rechte Ende des Bereichs markieren, in dem das gesuchte Element noch liegen könnte. 11 1.1. WAS IST INFORMATIK? START lies x ein L=1 R=n found=false L<=R && found==false nein gib found aus gib Index m aus ? ja END m=(L+R) / 2 a[m]==x ? ja found=true nein a[m]<x ? ja L=m+1 nein R=m−1 Abbildung 1.4: Flußdiagramm zum Binären Suchen Der Algorithmus BinSuch läßt sich auch in einer an die Umgangssprache angelehnten Notation formulieren. (a) Setze L = 1, R = n und found = FALSE. (b) (∗ Suchlauf ∗)1 Solange L ≤ R und found == FALSE gilt, wiederhole: 1 Durch (∗ Kommentar“ ∗) werden in Algorithmen Kommentare gekennzeichnet. ” 12 KAPITEL 1. EINFÜHRUNG Sei L ≤ m ≤ R beliebig; falls am == x setze found = TRUE; ansonsten falls am < x setze L = m + 1; sonst (∗ am > x ∗) setze R = m − 1 (c) Ergebnis wird mitgeteilt durch found. Bemerkung 1.2 Der aufmerksame Leser wird sich fragen, warum wir das Problem derartig kompli” ziert“ lösen. Es würde doch genügen, die Liste von links nach rechts zu durchsuchen und zu stoppen, falls das momentan betrachtete Element mit x übereinstimmt oder das Listenende erreicht wurde. Damit könnte man sogar auf die Sortiertheit der betrachteten Liste verzichten. Der Grund unserer Auswahl liegt aber in der Effizienz des Algorithmus (vgl. Abschnitt 5). 3 4. Korrektheitsnachweis, Verifikation Ein Programm heißt korrekt, wenn es die durch die Spezifikation geforderten Eigenschaften erfüllt. Den (formalen) Nachweis der Korrektheit nennt man Verifikation. Klassisch unterscheidet man zwei Erfüllbarkeitsbegriffe, die sich ausschließlich beim Terminierungsverhalten unterscheiden: • partielle Korrektheit: Falls der Algorithmus terminiert, liefert er die geforderten Ergebnisse. • totale Korrektheit: Der Algorithmus terminiert und liefert die geforderten Ergebnisse. ( totale Korrektheit = partielle Korrektheit + Termination“) ” In der Tat ist BinSuch im folgenden Sinne total korrekt: Theorem 1.3 Der Algorithmus BinSuch terminiert und liefert das geforderte Ergebnis, d.h.: found == true gilt bei Terminierung genau dann, wenn es einen Index m gibt mit am = x. 3 5. Aufwandsanalyse (Komplexitätsanalyse) Hierunter versteht man eine Analyse des Ressourcenbedarfs des Algorithmus bez üglich eines gewählten Komplexitätsmaßes. Im programmiersprachlichen Kontext ist es üblich, Laufzeit– (und gegebenenfalls Speicher–) Bedarf als Kriterien zu verwenden. Die Laufzeit wird dabei durch die Anzahl der auszuführenden Elementaroperationen (Zuweisungen, Vergleiche, etc. ) und der Speicherbedarf durch das Maximum der gleichzeitig relevanten elementaren Datenobjekte modelliert. Im Rahmen unseres Beispiels wollen wir die Anzahl der Schleifendurchläufe zählen, die notwendig sind, um den gesuchten Index m mit a m = x zu finden bzw. festzustellen, daß kein solcher Index existiert. Den Aufwand innerhalb der Schleife betrachten wir der Einfachheit halber als konstant. 1.1. WAS IST INFORMATIK? 13 Die Wahl von m ist bei jedem Schleifendurchlauf zunächst willkürlich und unerheblich für das letztendliche Resultat. Sie beeinflußt jedoch die Effizienz des Algorithmus. Selbstverständlich ist es erstrebenswert, in jedem Schritt möglichst viele Elemente zu eliminieren. Die beste Wahl von m ist damit das Element in der Mitte der Liste, weil so auf jeden Fall die Hälfte der verbliebenen Elemente ausgeschlossen wird. Im günstigsten Fall (best case) finden wir das gesuchte Element beim ersten Schleifendurchlauf. Im schlechtesten Fall (worst case) halbieren wir das betrachtete Intervall bei jedem Schleifendurchlauf solange, bis die Schleife wegen L > R abbricht. Da das Intervall anfangs n Elemente umfaßt – dies entspricht der Länge der Liste – und es bei jedem Schleifendurchlauf halbiert wird, ist dies höchstens log 2 (n)–mal möglich. Wir benötigen also im worst case nicht mehr als blog 2 (n)c + 1–Schleifendurchläufe. Bemerkung 1.4 Das vorgestellte Verfahren unterscheidet sich also in seiner Komplexität positiv vom naiven linearen Durchsuchen der Liste, das offensichtlich einen zu n proportionalen Zeitaufwand erfordert. Auf der anderen Seite erfordert es aber die Zusatzbedingung, daß die eingegebene Liste sortiert ist. Es ist daher bei der Organisation von Listen oft vorteilhaft, deren Elemente stets sortiert zu halten. Als drastisches Beispiel stelle man sich den Aufwand der Suche in einem Telefonbuch vor, das nicht alphabetisch sortiert ist! Diese Situation ist für die Komplexität von Programmen typisch: Zusätzliche Eigenschaften (wie hier die Sortiertheit) erlauben die Entwicklung von effizienteren (schnelleren) Algorithmen für die Problemlösung. 3 6. Programmkonstruktion Dabei wird der Kontext gewechselt, indem man sich mit einer Implementierung des Algorithmus beschäftigt. Die Fragestellung entspricht einer Anpassung bzw. Ergänzung der Spezifikation und des Algorithmus an den Kontext der tatsächlichen Kodierung in einer Programmiersprache. Typische Fragen sind: • Welche Programmiersprache steht zur Verfügung? (Wahl einer bestimmten Programmiersprache) • Wie kommen die Daten in den Rechner und wie wird das Ergebnis ausgegeben? (Festlegen der Ein/Ausgabe–Medien, die die Ein/Ausgabe–Portion des Programms bestimmen) • Wie wird das Programm gegliedert? (Strukturierung, Überlegung einer möglichst sinnvollen, sparsamen, lesbaren, anschaulichen, internen Organisation des Programms) Für diese Vorlesung wird Java als Programmiersprache gewählt. Im entsprechenden Kapitel wird auf die oben gestellten Fragen detailliert eingegangen. Hier wird lediglich der Algorithmus BinSuch in an Java angelehnter Notation angegeben. Dabei 14 KAPITEL 1. EINFÜHRUNG ist noch anzumerken, daß die Definition des Wertebereiches eines Objekts durch die Spezifikation seines Typs erfolgt. Seien also L, R und m vom Typ int (Dieser entspricht in Java den ganzen Zahlen.), a ein Array vom Typ int mit [1..n] (Dies entspricht einem n-stelligen Tupel ganzer Zahlen.) und found vom Typ boolean (Dieser besteht aus den Wahrheitswerten true und false.) und a[i] die i-te Komponente der Liste a. Dann sieht eine Java Implementierung des Algorithmus zur Binären Suche wie folgt aus: L = 1; R = n; found = false while ((L <= R) && (found == false)) { m = (L+R)/ 2 /* L <= m <= R */ if (a[m] == x) { found = true } else { if (a[m] < x) { L = m+1 } else { R = m-1 } } } 1.2 Information und Repräsentation Um über die reale Welt Aussagen treffen und Zusammenhänge darstellen zu können, werden Begriffe benötigt, mit denen Objekte oder Erscheinungen (Sinneseindrücke, Wahrnehmungen) beschrieben werden. Jeder Begriff beschreibt dabei einen bestimmten Ausschnitt (ein Objekt bzw. einen Zusammenhang/Eindruck unter vielen) der Realität. Begriffe können aus anderen Begriffen zusammengesetzt sein. Dieses Vorgehen ist ein systematischer Aufbau eines Begriffs durch Komposition von Begriffen. Beim Vorgang der Begriffsbildung wird abstrahiert. So enthält der Begriff Auto“ ” keine Information über Form, Farbe etc. der zugehörigen Objekte. Bei Ausführung der gedanklichen Zuordnung des Begriffs Auto“ zu einem speziellen Objekt wird also von ” individuellen Merkmalen abstrahiert. Das ist der Übergang von einem konkreten Objekt zu seiner ‘Idee’ im platonischen Sinne. Der zur Begriffsbildung inverse Vorgang der Zuordnung von Objekten zu einem Begriff ist das Verstehen oder Begreifen. Unter Information verstehen wir den abstrakten Bedeutungsgehalt eines Begriffs. Damit wir Information kommunizieren können, wird eine schematische, formalisierte Darstellung 1.2. INFORMATION UND REPRÄSENTATION 15 benötigt: die Repräsentation. Diese kann z.B. akustischer oder visueller Natur sein. Für unser Auto wären die Zeichenreihen Auto“ oder voiture“, die entsprechenden Lautfolgen ” ” (kontinuierliche akustische Darstellung), eine Folge von Morsesignalen (diskrete akustische Signale) oder eine Piktogramm–Darstellung denkbar. Ein solcher Repräsentant (Träger) einer Information heißt Nachricht oder auch Datum. Beispiel 1.5 (Visuelle Repräsentationen der Zahl drei“) ” • Dezimal: 3 • Binär: 11 • Unär: ||| 3 Darstellung und Interpretation bestimmen, abstrakt gesehen, die Übergänge von der Information zur Repräsentation und zurück. Wie schon gesehen, kann eine Information auf verschiedene Weisen repräsentiert werden. Die Festlegung oder Konzeption eines geeigneten Repräsentationssystems ( Sprache“) zusammen mit einer adäquaten Begriffsbildung ” ist eine zentrale Aufgabe der Informatik, die wir als Definition eines Semantikschemas bezeichnen wollen. Die Interpretation (Deutung) liefert zu jeder Repräsentation ihre Semantik (Bedeutung). Ohne Interpretation sind alle Repräsentationen bedeutungsleer. Erst die Zuordnung von Bedeutungen macht die Repräsentation zum Informationsträger. Repräsentationen können unterschiedlich interpretiert werden. Zum Beispiel kann X“ ein ” Buchstabe oder eine Darstellung der Zahl 10 sein, Mutter“ ein Elternteil oder das Ge” genstück zur Schraube, und auch der englische Satz Fruit flies like a banana“ läßt sich ” auf zweierlei Weise interpretieren. In vielen Fällen erlaubt der Kontext eine eindeutige Interpretation. Aber auch das ist zumindest im umgangssprachlichen Bereich nicht garantiert. Im Gegensatz dazu wird im programmiersprachlichen Bereich großer Wert auf eine eindeutige Interpretierbarkeit gelegt. Wie anders könnte man sonst die Zuverläßigkeit von Softwaresystemen gewährleisten. In der Tat ist es hier aus Effizienzgründen im allgemeinen ungenügend, kontextabhängig eindeutig zu sein. Alles das ist beim Design von Programmiersprachen zu beachten. Im täglichen Leben wird zwischen Repräsentation und Information (als Resultat einer Standardinterpretation) in der Regel nicht unterschieden. In der Informatik gibt es a priori keine Standardinterpretation. Das erhöht den Spielraum beim Design der Semantikschemata, macht aber eine explizite begriffliche Trennung zwischen dem abstrakten Informationsgehalt und der äußeren Form unbedingt notwendig. Wir haben es also hier mit der in Abbildung 1.5 illustrierten Situation zu tun. 16 KAPITEL 1. EINFÜHRUNG Objekte der realen Welt Begriffsbildung Verstehen Abstraktion Begreifen Begriffe, Information Darstellung Interpretation Repräsentationen Abbildung 1.5: Abstraktionsebenen Beispiel 1.6 Zur Verdeutlichung der verschiedenen Abstraktionsebenen ist in Abbildung 1.6 eine spezielle Instanz des Schemas aus Abbildung 1.5 gegeben. 3 ein Apfel Abstraktion (nicht alle Äpfel sind gleich) Verstehen (sich einen Apfel vorstellen) Begriffe: Apfel, pomme, apple Darstellung Interpretation Repräsentation "A" "p" "f" "e" "l" Abbildung 1.6: Eine Beispielinstanz 1.2. INFORMATION UND REPRÄSENTATION 1.2.1 17 Semantikschemata Hier soll der Zusammenhang zwischen Information und Repräsentation formal erfaßt werden. Dazu führen wir den Begriff des Semantikschemas ein. Definition 1.7 (Semantikschema) Ein Semantikschema ist ein Tupel hR, I, [[ · ]]i mit • R: Menge der Repräsentationen oder Nachrichten, • I: Menge der Begriffe oder Informationen, • [[ · ]] ⊆ R × I: Semantik(–Relation) oder Interpretation. Falls hr, ii ∈ [[ · ]] , so ist i eine Information zur Repräsentation r. Im folgenden schreiben wir oft i ∈ [[ r ]] anstelle von hi, ri ∈ [[ · ]]. Ist [[ r ]] einelementig, so schreiben wir auch i = [[ r ]]. 3 Im folgenden wollen wir den neu eingeführten Begriff anhand einer Reihe von Semantikschemata für die Menge der natürlichen Zahlen illustrieren. Dabei sollen gleichzeitig auch die später definierten Eigenschaften von Semantikschemata motiviert werden. Beispiel 1.8 (Unärdarstellung natürlicher Zahlen) Eine Interpretation der Unärdarstellung von natürlichen Zahlen ist durch das folgende Semantikschema hRu , I u , [[ · ]]u i gegeben mit: • Ru =df {|, ||, |||, . . . }, • I u =df N =df {1, 2, . . . }: Natürliche Zahlen (als Begriffe bzw. Information, nicht als ihre Darstellung im Dezimalsystem! Siehe auch nächstes Beispiel) und • [[ · ]]u =df {h|| . . . |, ki | k ∈ N}. Die Semantik einer Nachricht aus k Strichen ist also | {z } k die natürliche Zahl k. 3 Beispiel 1.9 (Dezimaldarstellung nat ürlichen Zahlen) Das folgende Semantikschema hR d , I d , [[ · ]]d i für die Dezimaldarstellung natürlichen Zahlen verdeutlicht noch einmal den Unterschied zwischen der Information und ihrer Repräsentation: • Rd =df {(di )0≤i≤n | di ∈ {0, . . . , 9} f.a. 0 ≤ i ≤ n ∧ n ∈ N0 }. 18 KAPITEL 1. EINFÜHRUNG • I d =df N0 =df {0, 1, 2, . . . }: Natürliche Zahlen (als Begriffe bzw. Informationen, nicht als ihre Notation im Dezimalsystem!) und • [[ · ]]d =df {h(di )0≤i≤n , n P di ∗ 10n−i i | n ∈ N0 } = {(0, 0), (1, 1), (2, 2), . . .}. i=0 3 Repräsentation und Information werden oft gleich geschrieben. Dies bedeutet aber nicht, daß auf die Unterscheidung verzichtet wird. Ob nun die Zahl oder ihre Darstellung gemeint ist, muß dann aus dem Kontext geschlossen werden. Beispiel 1.10 (Binärdarstellung natürlicher Zahlen) Das folgende Semantikschema hR b , I b , [[ · ]]b i arbeitet auf der Binärdarstellung der natürlichen Zahlen. • Rb = {(di )0≤i≤n | di ∈ {0, 1} f.a. 0 ≤ i ≤ n ∧ n ∈ N0 }. • I b =df N0 =df {0, 1, 2, . . . }: Natürliche Zahlen (wieder als Begriffe bzw. Informationen, nicht als ihre Notation im Dezimalsystem). • [[ · ]]b =df {h(di )0≤i≤n , n P di ∗ 2n−i i | n ∈ N0 } = {(0, 0), (1, 1), (10, 2), . . .}. i=0 3 Beispiel 1.11 (p-adische Darstellung nat ürlichen Zahlen) Als Verallgemeinerung der vorherigen Beispiele kann die p-adische Darstellung der nat ürlichen Zahlen betrachtet werden. Sei p Repräsentation der p-ten natürlichen Zahl (p ≥ 1). Dann erhalten wir folgendes Semantikschema hR p , I p , [[ · ]]p i: • Rp =df {(di )0≤i≤n | di ∈ {0, . . . , p-1} f.a. 0 ≤ i ≤ n ∧ n ∈ N0 }. • I p =df N0 = {0, 1, 2, . . . }: Natürliche Zahlen (wieder als Begriffe bzw. Informationen, nicht als ihre Notation im Dezimalsystem). • [[ · ]]p =df {h(di )0≤i≤n , n P di ∗ pn−i i | n ∈ N0 }. i=0 Von besonderer Bedeutung ist hier die 16-adische Darstellung, auch Hexadezimaldarstellung genannt, bei der üblicherweise die Buchstaben A bis F anstelle der Ziffernreps̈entationen für 10 bis 15 treten. Wir bezeichnen diese mit hR h , I h , [[ · ]]h i. 3 Beispiel 1.12 (Semantikschema für Kardinalzahlen in MODULA–2) Bei den Kardinalzahlen von MODULA–2 handelt es sich um einen beschränkten Bereich der natürlichen Zahlen inklusive der Null. Sie werden intern als eine Bitfolge der Länge 1.2. INFORMATION UND REPRÄSENTATION 19 B (normalerweise B = 16 oder B = 32) abgelegt (vgl. Beispiel 7), so daß nur Zahlen aus dem Bereich {0, 1, . . . , 2B − 1} abgelegt werden können. Das zugehörige Semantikschema hRK , I K , [[ · ]]K i hat folgende Form: • RK =df {0, 1, . . . , 2B -1}, • I K =df N0 = {0, 1, . . .} und • [[ · ]]K =df {hk, ki | k ∈ RK }. 3 Beispiel 1.13 (Vervollständigte Kardinalzahlen) Zahlen außerhalb des im vorigen Beispiel dargestellten Bereichs können intern nicht abgespeichert werden. Treten sie als Rechenergebnis auf, so kommt es zu einem Unter– bzw. Überlauf. Dies führt meist zu einem Fehlerabbruch der Berechnung und wird durch folgendes Semantikschema hRuo , I uo , [[ · ]]uo i mit • Ruo =df Rk ∪ {underflow, overflow}, • I uo =df Z und • [[ r ]]uo =df modelliert. falls r = underflow {i ∈ Z | i < 0} {i ∈ Z | i > 2B − 1} falls r = overflow [[ r ]]K sonst 3 Das Beispiel zeigt, daß eine Repräsentation, wie hier zum Beispiel “underflow”, durchaus uneindeutig, das heißt durch mehrere Informationen interpretiert werden kann. Im folgenden werden einige zentrale Eigenschaften von Semantikschemata definiert. Definition 1.14 Ein Semantikschema hR, I, [[ · ]]i heißt • total, falls ∀ r ∈ R. [[ r ]] 6= ∅. • eindeutig gdw. [[ · ]] eine totale Funktion ist, d.h. falls ∀ r ∈ R. |{i | hr, ii ∈ [[ · ]]}| = 1. Ist [[ · ]] eindeutig, so heisst [[ · ]] 20 KAPITEL 1. EINFÜHRUNG • injektiv gdw. [[ · ]] injektiv ist, d.h. falls ∀ r1 , r2 ∈ R. [[ r1 ]] = [[ r2 ]] ⇒ r1 = r2 . • surjektiv gdw. [[ · ]] surjektiv ist, d.h. falls ∀ i ∈ I. ∃ r ∈ R. [[ r ]] = i 3 Alle Semantikschemata aus den Beispielen 1.8 bis 1.13 sind total, da jeder Repräsentation mindestens eine Information zugeordnet wird. Würde man allerdings beispielsweise in Beispiel 1.10 die Informationsmenge auf N beschränken, so wäre das Semantikschema nicht mehr total, da es für die Repräsentation 0 keine entsprechende Information mehr geben würde. Die Semantikschmata sind auch bis auf Beispiel 1.13 alle eindeutig, da jeder Repräsentation genau eine Information zugeordnet wird, was im Falle von Beipiel 1.13 wie bereits dargelegt - nicht der Fall ist. Unter den eindeutigen Semantikschemata sind die Unärdarstellung natürlicher Zahlen (Bsp. 1.8) und die MODULA-2 Kardianalzahlen (Bsp. 1.12) injektiv, denn es gibt keine Information, die verschiedene Repräsentationen besitzt. Auf der anderen Seite ist z.B. das Semantikschema für die Binärdarstellung natürlicher Zahlen (Bsp. 1.10) nicht injektiv. Dieses liegt daran, daß die Repräsentationen führende Nullen zulassen. So wird z.B. sowohl 1 als auch 01 als natürliche Zahl 1 interpretiert. Dieser “Defekt” ließe sich aber leicht beseitigen, indem die Repräsentationsmenge des Semantikschemas eingeschränkt würde auf: Rb =df {(di )0≤i≤n | di ∈ {0, 1} f.a. 0 ≤ i ≤ n ∧ n ∈ N0 ∧ ((d0 6= 0) ∨ (n = 0)) }. Schließlich sind für unsere Beispiele alle eindeutigen Semantikschemata bis auf die MODULA-2 Kardinalzahlen (Bsp. 1.12) surjektiv, denn jede Information besitzt auch mindestens eine zugehörige Repräsentation. Im Falle der MODULA-2 Kardianalzahlen ist dieses aber für natürliche Zahlen größer als 2B − 1 nicht der Fall. 1.2.2 Nachrichtenverarbeitung Formal gesehen ist eine Nachrichtenverarbeitung eine Abbildung, die eine Repräsentation auf eine Repräsentation abbildet. Es handelt sich also um eine ‘Repräsentationstransformation’, die zunächst vollkommen interpretationsunabhängig ist. Definition 1.15 (Äquivalenztransformation, Kongruenztransformation) Gegeben seien zwei Semantikschemata hR 1 , I 1 , [[ · ]]1 i und hR2 , I 2 , [[ · ]]2 i. Dann heißt eine Funktion f : R1 → R2 Nachrichtenverarbeitung. Abbildung 1.7 illustriert diese Situation. 21 1.2. INFORMATION UND REPRÄSENTATION R1 f R2 [[ · ]]2 [[ · ]]1 I2 I1 Abbildung 1.7: Nachrichtenverarbeitung • f heißt Kongruenztransformation, falls ∀ r, r 0 ∈ R1 . [[ r ]]1 = [[ r 0 ]]1 ⇒ [[ f (r) ]]2 = [[ f (r 0 ) ]]2 ( Merke: Kongruenztransformationen erhalten die Bedeutungsgleichheit.“) ” • Oft liegt wie in Abbildung 1.8 illustriert die Situation vor, daß I 1 = I 2 . In dieser Situation heißt f Äquivalenztransformation, falls ∀ r ∈ R1 . [[ r ]]1 = [[ f (r) ]]2 . ( Merke: Äquivalenztransformationen erhalten die Bedeutung.“) ” 3 R1 f R2 [[ · ]]2 [[ · ]]1 I1 = I2 Abbildung 1.8: Situation für Äquivalenztransformationen Beispiel 1.16 (Nachrichtenverarbeitungen) Ein Beispiel für eine Nachrichtenverarbeitung ist eine Konversion zwischen verschiedenen Zahldarstellungen. Die Funktion f : R h → Rb , die definiert ist durch ¯ 0 ) · · · f(d ¯ n ), f ((di )0≤i≤n ) = f(d ordnet jeder Hexadezimalzahl ihre Binärzahldarstellung zu, wobei die Hilfsfunktion f¯ : {0, . . . , F} → {0, 1}4 jeder Hexadezimalziffer ihre zugehörige Binärdarstellung zuordnet: ḡ(0) = 0000, ḡ(1) = 0001, ḡ(2) = 0010, . . . , ḡ(F) = 1111. 22 KAPITEL 1. EINFÜHRUNG Die Nachrichtenverarbeitung f ist sowohl eine Kongruenztransformation als auch eine Äquivalenztransformation. Die Funktion g : Rb → Ruo , die definiert ist durch ( [[ x ]]b falls 0 ≤ [[ x ]]b < 2B g(x) = overflow sonst ist ein Beispiel einer Kongruenztransformation, die keine Äquivalenztransformation ist. Letzteres ist zwar formal ohnehin durch die unterschiedlichen Informationsmengen ausgeschlossen. Diese Einschränkung ließe sich aber leicht beseitigen, indem man I b zu Z erweitert. Selbst dann bleiben aber Information duch g nicht erhalten, denn es gilt beispielsweise für die Binärrepräsentation x von 2B : 2B = [[ x ]]b 6= [[ g(x) ]]b = [[ overflow ]]uo = {i ∈ Z | i > 2B − 1} = {2B , 2B+1 , . . .} . 3 Es gilt jedoch folgender unmittelbarer Zusammenhang: Lemma 1.17 Seien hR1 , I, [[ · ]]1 i und hR2 , I, [[ · ]]2 i Semantikschemata mit gleicher Informationsmenge und f : R1 → R2 eine Nachrichtenverarbeitung. Dann gilt: Falls f eine Äquivalenztransformation ist, so ist f auch eine Kongruenztransformation. 3 1.2.3 Kodierung und Verschlüsselung Eine Äquivalenztransformation wird oft auch Kodierung genannt. Kodierungen verwendet man zum Beispiel zur Übertragung von Nachrichten durch binäre Signale (R2 = {0, 1}∗ ) oder zur Fehlererkennung bei solchen Übertragungen. Dabei bezeichnet M ∗ für eine Menge M die Menge der leeren oder nicht-leeren endlichen Folgen mit Elementen aus M: M ∗ = {m1 · · · mk | m1 , . . . , mk ∈ M, k ≥ 0}. Elemente w ∈ M ∗ bezeichnet man auch als Worte über M . Das leere Wort (k = 0) wird mit ε bezeichnet. Eine Kodierung f heißt Verschlüsselung, wenn f (r) weit weg von der Standardinter” pretation ist“, d.h. die Bedeutung [[ r ]] 1 durch f (r) verdeckt wird. Beispiel 1.18 (Caesar Chiffre) Hier wird ein zyklischer Shift der Buchstaben um 3 (Transposition, Permutation von Buchstaben) durchgeführt: A → D , B → E , . . . , Z → C. 3 1.3. STRUKTUR UND INVARIANZ 23 Eine andere Verschlüsselung ist das Rückwärtsschreiben. Zeichen und Bedeutungen Gerade in der Informatik ist es wichtig, immer genau zu wissen, wie Zeichen situationsgerecht interpretiert werden müssen. Besonders betrifft das die vielen Situationen, in denen mit Varianten von Gleichheiten umgegangen wird. Die folgende Tabelle fasst einige zentrale Situationen zusammen und zeigt, wie wir die Unterschiede weitestgehend auch direkt durch die Zeichen verdeutlichen werden. Tatsächlich kann man in der Literatur in all diesen Situationen auch ein einfaches ’=’ finden. =df = == = definierend: die linke Seite benennt das Objekt der rechten Seite Zuweisung (Java-Syntax); entspricht “:=” etwa in Pascal Wertegleichheit (Java-Syntax) steht für syntaktische Gleichheit von Booleschen Termen und arithmetischen Ausdrücken Beachte aber die Existenz von Abkürzungen, z.B. x[t/x] = t die semantische Gleichheit wird explizit ausgedrückt, z.B. [[ 2x ]] = [[ x + x ]] die Relation = in Formeln wird semantisch interpretiert, z.B. x = 5 ∧ y = 10 (vgl. Hoare-Logik) Es ist Teil des Lernziels, die richtige Interpretation der Gleichheit im jeweiligen Kontext sicher zu beherrschen. 1.3 Struktur und Invarianz Grundlegend für den Aufbau komplexer Objekte ist die systematische, hierarchische Konstruktion größerer Bausteine aus bereits ‘produzierten’ kleineren Bausteinen. Besonders wichtig ist dieses Prinzip bei der Programmierung großer Softwaresysteme. Voraussetzung für den Erfolg dieses Vorgehens ist, daß die Konstruktionsumgebung ein hierarchisches Vorgehen entlang der inhärenten Problemstruktur unterstützt. Insbesondere ist es erforderlich, daß die Repräsentationssprache des zugrundeliegenden Informationssystems selbst strukturiert ist, und zwar in einer Weise, daß Teilstrukturen der Repräsentation in ‘natürlicher’ Weise Teilstrukturen der Informationsebene entsprechen: Repräsentations- und Informationsstruktur m üssen zueinander passen. Formal bedeutet das, daß die Konstruktoren oder Operatoren, mit denen Bausteine zusammengesetzt werden können, Kongruenztranformationen induzieren. Diesen Aspekt wollen 24 KAPITEL 1. EINFÜHRUNG wir später noch genauer betrachten. Erste Voraussetzung strukturierten Vorgehens ist die induktive Definition der Repräsentationssprache. 1.3.1 Induktives Definieren Die induktive Definition einer Menge erfolgt durch Angabe 1. einer Menge elementarer Grundobjekte oder atomarer Bausteine und 2. einer Menge von Operatoren, die es erlauben, kleinere Bausteine entsprechend einer Konstruktionsvorschrift zu größeren Einheiten zusammenzusetzen. Formal sind solche Operatoren Funktionen f einer jeweils vorgebenen Stelligkeit n (die Anzahl der bei der Konstruktion mit Hilfe von f nötigen kleineren Bausteine). ‘Wohlkonstruierte’ neue Objekte haben dann die Gestalt f (x 1 , · · · , xn ), wobei die xi die verwendeten kleineren Bausteine bezeichnen. Die induktiv definierte Menge besteht also exakt aus den in endlich vielen Schritten ‘wohlkonstruierbaren’ Bausteinen. Syntax Boolescher Terme Definition 1.19 (Syntax) Sei ID eine Menge von Identifikatoren (Variablen), z.B. ID = {x, y, z, ...}. Die Menge BT aller Booleschen Terme über ID ist wie folgt induktiv definiert: 1. true, false und Identifikatoren id aus ID, sind Boolesche Terme. Sie werden auch atomare“ Boolesche Terme oder kurz Atome genannt. ” 2. Sind t1 und t2 Boolesche Terme, so sind auch • (¬t1 ), die Negation von t1 , • (t1 ∧ t2 ), die Konjunktion von t1 und t2 und • (t1 ∨ t2 ), die Disjunktion von t1 und t2 Boolesche Terme. 3. Weitere als die aus 1) und 2) konstruierbaren Booleschen Terme gibt es nicht. Ein Boolescher Term t1 , der als Baustein bei der Konstruktion eines Booleschen Terms t 2 auftritt, heißt Teilterm von t2 . 3 1.3. STRUKTUR UND INVARIANZ 25 Beispiel 1.20 (¬(t1 ∨ (false ∧ x))) ist ein Boolescher Term mit den folgenden Teiltermen: t1 , false, x, (false ∧ x), (t1 ∨ (false ∧ x)), (¬(t1 ∨ (false ∧ x))). 3 In der Informatik hat sich ein spezielles Format zur induktiven Definition syntaktischer Strukturen (Sprachen) durchgesetzt, die (erweiterte) Backus-Naur-Form (BNF). Eine BNF besteht aus endlich vielen Regeln der Form linke Seite ::= rechte Seite Regeln werden im Zusammenhang mit BNFs oft auch als Produktionen bezeichnet. Die linken“ und rechten Seiten“ basieren auf disjunkten Mengen T und N : ” ” • T ist eine endliche Menge sogenannter Terminalsymbole. Terminalsymbole sind atomare syntaktische Symbole der Sprache, die durch die BNF beschrieben wird. • N ist eine endliche Menge sogenannter Nichtterminalsymbole. Nichtterminalsymbole dienen als Hilfsbezeichner. Jede linke Regelseite besteht aus einem Nichtterminalsymbol A ∈ N . Rechte Regelseiten sind leere oder nichtleere Folgen von Terminal- oder Nichtterminalsymbolen. In einer Regel LS ::= RS gilt also LS ∈ N und RS ∈ (N ∪ T ) ∗ . Dabei ist (N ∪ T )∗ =df {w1 · · · wk | w1 , . . . , wk ∈ (N ∪ T ), k ≥ 0} . Insbesondere ist die leere Zeichenreihe ε als rechte Seite einer Regel zugelassen. Konvention 1.21 Enthält eine BNF mehrere Regeln mit identischen linken Seiten, etwa LS ::= RS 1 , LS ::= RS 2 , ..., LS ::= RS n so schreiben wir für diese Regeln auch kurz LS ::= RS 1 | RS 2 | ... | RS n Der Konstruktor |“ ist also als Regelalternative“ zu verstehen. ” ” 3 26 KAPITEL 1. EINFÜHRUNG Beispiel 1.22 (BNF für Boolesche Terme) Die Booleschen Terme sind durch die folgende BNF mit T = df {true, false, ¬, ∧, ∨, (, )} ∪ ID und N =df {ABT , BT } charakterisiert (dabei sei id ∈ ID): 2 ABT ::= true | false | id BT ::= ABT | (¬BT ) | (BT ∧ BT ) | (BT ∨ BT ) Alternativ kann man auch die folgende BNF mit nur einem Nichtterminalsymbol verwenden: BT ::= true | false | id | (¬BT ) | (BT ∧ BT ) | (BT ∨ BT ) In diesem Beispiel nehmen wir an, daß ID, die Menge der Identifikatoren, endlich ist und verstehen die Regel ABT ::= id (bzw. BT ::= id) als Abkürzung für je eine Regel ABT ::= x (bzw. BT ::= x) für jeden Identifikator x ∈ ID. 3 Bemerkung 1.23 (BNF-artige Definitionen) Oft arbeitet man auch mit sogenannten BNF-artigen Definitionen, in denen man auf die Forderung nach Endlichkeit von N , T und der Menge der Regeln verzichtet. Fasst man das Regelsystem aus Beispiel 1.22 als BNF-artige Definition auf, so wird dadurch die Menge der Booleschen Terme auch definiert, wenn ID unendlich viele Identifikatoren enthält. Die im folgenden definierten Begriffe übertragen sich in natürlicher Weise auf BNF-artige Definitionen. 3 Eine BNF induziert eine Ableitungsrelation ⇒ ⊆ (N ∪T ) ∗ ×(N ∪T )∗ , die wie folgt definiert ist: w ⇒ w0 gdw. es gibt w1 , w2 ∈ (N ∪ T )∗ und eine Regel A ::= r, so daß w = w1 Aw2 und w0 = w1 rw2 . In der beschriebenen Situation sagt man auch, dass w 0 aus w durch Anwenden der Regel A ::= r abgeleitet werden kann. In einem Ableitungsschritt, in dem man eine Regel A ::= r anwendet, ersetzt man also in einem Wort w ein Vorkommen von A durch r. Dabei bleibt alles, was in w links oder rechts von dem ersetzten Vorkommen von A steht, unverändert. Die oben angegebene Definition von ⇒ schreibt man auch kürzer als w1 Aw2 ⇒ w1 rw2 , falls A ::= r eine Regel der BNF ist. Eine Folge w1 , . . . , wk von Wörtern über N ∪ T (d.h. w1 , . . . , wk ∈ (N ∪ T )∗ ) heißt Ableitunsfolge, wenn wi ⇒ wi+1 für alle i ∈ {1, . . . , n−1} gilt. Man sagt, ein Wort w 0 ∈ (N ∪T )∗ lasse sich aus einem Wort w ∈ (N ∪ T )∗ ableiten, wenn es eine Ableitungsfolge w 1 , . . . , wk mit w = w1 und w0 = wk gibt. In diesem Fall schreibt man auch w ⇒ ∗ w0 . Für ein gegebenes Nichtterminalsymbol A besteht die von A generierte Sprache aus genau den Wörtern 2 Dabei stehen intuitiv die Bezeichner ABT für atomare Boolesche Terme und BT für Boolesche Terme. 27 1.3. STRUKTUR UND INVARIANZ w ∈ T ∗ , die sich aus A ableiten lassen. Die von A generierte Sprache bezeichnet man als L(A). Es gilt also: L(A) =df {w ∈ T ∗ | A ⇒∗ w} . Beachte, daß die Wörter in der von A abgeleiteten Sprachen keine Nichtterminalsymbole enthalten dürfen. Beispiel 1.24 (Ableitungsfolge, generierte Sprache) Als Beispiel geben wir zur BNF aus Beispiel 1.22 eine Ableitungsfolge für das Wort ((¬x)∧ (true ∨ y)) an: BT ⇒ (BT ∧ BT ) ⇒ (BT ∧ (BT ∨ BT )) ⇒ (BT ∧ (true ∨ BT )) ⇒ ((¬BT ) ∧ (true ∨ BT )) ⇒ ((¬BT ) ∧ (true ∨ y)) ⇒ ((¬x) ∧ (true ∨ y)) Diese Ableitunsfolge zeigt, daß ((¬x) ∧ (true ∨ y)) zur von BT generierten Sprache L(BT ) gehört. Beachte aber, daß das Wort (BT ∧ BT ) nicht zur von BT generierten Sprache gehört, da es Nichtterminalsymbole enthält. 3 Für den in Beispiel 1.22 abgeleiteten Booleschen Term gibt es weitere Ableitungsfolgen, in denen unabhängige Teilterme in anderer Reihenfolge abgeleitet werden, z.B.: BT ⇒ ⇒ ⇒ ⇒ ⇒ ⇒ (BT ∧ BT ) ((¬BT ) ∧ BT ) ((¬x) ∧ BT ) ((¬x) ∧ (BT ∨ BT )) ((¬x) ∧ (true ∨ BT )) ((¬x) ∧ (true ∨ y)) Die Essenz aller dieser Ableitungsfolgen kommt im sogenannten Ableitungsbaum zum Ausdruck, der für alle diese Ableitungsfolgen gleich ist: ( ( ¬ x ) ∧ BT BT ( true ∨ BT BT y ) ) BT BT Genauer ist ein Ableitungsbaum ein geordneter Baum (siehe Vorlesung “Datenstrukturen, Algorithmen und Programmierung 2”), dessen Blätter mit Terminal- oder Nichtterminalsymbolen und dessen innere Knoten mit Nichtterminalsymbolen benannt sind und f ür den für jeden Knoten v gilt: 28 KAPITEL 1. EINFÜHRUNG Ist v mit dem Nichtterminalsymbol A benannt und seine Söhne von links nach rechts mit den Symbolen s1 , . . . , sk ∈ (N ∪ T ) so ist A ::= s1 · · · sk eine Regel der BNF. Insbesondere kann also ein Blatt des Baumes nur dann mit einem Nichtterminalsymbol A benannt sein, wenn es in der BNF eine Regel A ::= ε gibt. Man kann zeigen, dass ein Wort w ∈ T ∗ genau dann in der von A generierten Sprache liegt, wenn es einen Ableitungsbaum gibt, dessen Wurzel mit A benannt ist und an dessen mit Terminalsymbolen benannten Blättern w steht, wenn man sie von links nach rechts liest. Eng verwandt mit dem Begriff BNF ist der Begriff kontextfreier Grammatiken. Eine kontextfreie Grammatik ist ein Quadrupel G = (N , T , S, P ), wobei • N eine endliche Menge von Nichtterminalsymbolen, • T eine endliche Menge von Terminalsymbolen, • S ∈ N ein ausgezeichnetes Nichtterminalsymbol, das sogenannte Startsymbol, und • P ⊆ N ×(N ∪T )∗ eine endliche Menge von Regeln (Produktionen) ist. Dabei schreibt man wie bei einer BNF eine Regel aus P oft in der Form LS ::= RS. Eine kontextfreie Grammatik besteht also aus der durch N , T und P gegebenen BNF zusammen mit einem Startsymbol S ∈ N . Die Sprache von G ist die Sprache, die vom Startsymbol S generiert wird. Sie wird mir L(G) bezeichnet. Beispiel 1.25 (Kontextfreie Grammatik f ür Boolesche Terme) Wähle N , T so wie in Beispiel 1.22 und wähle P als die Menge der in Beispiel 1.22 angegebenen Regeln. Dann ist die Sprache der Grammatik G = (N , T , BT , P ) die Menge der Booleschen Terme. Dabei nehmen wir wieder an, daß ID endlich ist. 3 Bemerkung 1.23 gilt analog auch für kontextfreie Grammatiken und führt dann zum Begriff der kontextfrei-artigen Grammatiken. 1.3.2 Induktives Beweisen Induktives Beweisen bedeutet “Beweisen entlang der induktiven Struktur” der zugrundeliegenden Objekte. Klassisch sind Beweise durch vollst ändige Induktion, die der induktiven Struktur der natürlichen Zahlen folgen: 1.3. STRUKTUR UND INVARIANZ 29 Jede natürliche Zahl entsteht durch eine endliche Anwendung der Nachfolgerfunktion ‘+ 1’. Formale Grundlage hierfür sind die Peano-Axiome: Axiomatische Begründung: die Peano-Axiome P1 1 ist eine natürliche Zahl: 1 ∈ N. P2 Jede natürliche Zahl besitzt eine eindeutig bestimmte natürliche Zahl als Nachfolger: ∀n ∈ N ∃m ∈ N. m = succ(n) ∧ (∀m0 ∈ N. m0 = succ(n) ⇒ m = m0 ) P3 1 ist nicht Nachfolger einer natürlichen Zahl: ¬∃n ∈ N. 1 = succ(n) P4 Verschiedene natürliche Zahlen haben verschiedene Nachfolger: ∀m, n ∈ N. n 6= m ⇒ succ(n) 6= succ(m) P5 Axiom der vollständigen Induktion: Ist eine Aussage über natürliche Zahlen für 1 wahr und läßt sich ihre Gültigkeit für jede größere natürliche Zahl aus der Gültigkeit der Aussage für ihren Vorgänger ableiten, dann ist sie für jede natürliche Zahl wahr. ⇒ ∀ n ∈ N. A(n) . A(1) ∧ ∀ n ∈ N. A(n) ⇒ A(n + 1) Bemerkung: Induktionsbeweise erlauben es, Eigenschaften für unendlich viele Objekte, z.B. die natürlichen Zahlen, in einheitlicher Weise zu verifizieren. Im folgenden betrachten wir einige typische Beispiele für Eigenschaften, die am besten mit vollständiger Induktion bewiesen werden. Für alle n ∈ N gilt: 1. n P i=1 2. n P i= n∗(n+1) , 2 Summe der ersten n natürlichen Zahlen. (2i − 1) = n2 , Summe der ersten n ungeraden Zahlen. i=1 3. n P 2i = n ∗ (n + 1), Summe der ersten n geraden Zahlen. i=1 4. n P i2 = n∗(n+1)∗(2n+1) , 6 i3 = n2 ∗(n+1)2 , 4 i=1 5. n P i=1 Summe der ersten n Quadratzahlen. Summe der ersten n Kubikzahlen. 30 KAPITEL 1. EINFÜHRUNG 6. Es gibt n! Permutationen über n Objekten. 7. Es gibt 2n Teilmengen von n–elementigen Mengen. 8. Es gibt 2n Binärworte der Länge n. Das Beweisprinzip der vollständigen Induktion, wie wir es bisher kennengelernt haben, ist in der Anwendung oft zu unflexibel. Als Beispiel dazu wollen wir eine Aussage über die Folge der Fibonacci–Zahlen betrachten. Diese sind wie folgt induktiv definiert (vgl. auch Abschnitt 1.3.1 und ): (vgl. auch Abschnitt 1.3.1): fib(0) =df fib(1) =df 1; fib(n+1) =df fib(n)+fib(n-1). Die zu beweisende Aussage lautet: ∀ n ∈ N. fib(n) ≤ 2n . Offensichtlich läßt sich der Beweis mittels vollständiger Induktion nicht unmittelbar führen, da im Induktionsschluß nicht nur die Induktionsvoraussetzung für den Vorgänger n − 1 von n, sondern auch für den Vorvorgänger n − 2 von n benötigt wird. Der folgende Satz, welcher das vollständige Induktionsprinzip verallgemeinert, verschafft einen Ausweg aus dem Dilemma. Satz 1.26 (Verallgemeinertes Induktionsprinzip) Läßt sich eine Aussage über natürliche Zahlen für jede natürliche Zahl aus der Gültigkeit der Aussage für alle kleineren natürlichen Zahlen ableiten, dann ist sie für jede natürliche Zahl wahr. ∀n ∈ N. ∀ m < n. A(m) ⇒ A(n) ⇒ ∀ n ∈ N. A(n) . 3 Beweis Zur Abkürzung bezeichnen wir in dieser Aufgabe einen Induktionsbeweis für Eigenschaft A, der gemäß dem Prinzip der vollständigen Induktion geführt wird, mit VSI (A) und einen Induktionsbeweis für Eigenschaft A, der gemäß dem Prinzip der verallgemeinerten Induktion geführt wird, mit VAI (A). Weiterhin bezeichen wir Prämisse und Konklusion von Induktionsbeweisen für eine Eigenschaft A durch Voranstellen der Buchstaben P und K. Es ergibt sich also folgendes Bild für die verwendeten Notationen: ∀n ∈ N. ∀ m < n. A(m) ⇒ A(n) ⇒ ∀ n ∈ N. A(n) {z } | | {z } PVAI (A) KVAI (A) VAI (A) 31 1.3. STRUKTUR UND INVARIANZ und A(1) ∧ | ∀ n ∈ N. A(n) ⇒ A(n + 1) ⇒ ∀ n ∈ N. A(n) | {z } {z } VSI (A) KVSI (A) PVSI (A) Sei A : N → B eine Aussage, die mittels verallgemeinerter Induktion bewiesen werden kann, also durch einen Beweis VAI (A). Dann zeigen wir, daß ein solcher Beweis auch immer durch einen Beweis mittels vollständiger Induktion geführt werden kann. Dieser Beweis wird allerdings für eine Aussage A0 geführt, die definiert ist wie folgt: ∀ n ∈ N. A0 (n) =df ∀ m < n. A(n) VAI (A), also die Implikation PVAI (A) ⇒ KVAI (A), wird nun dadurch gezeigt, dass die Implikationskette (1) PVAI (A) ⇒ PVSI (A0 ) VSI (A0 ) ⇒ (2) KVSI (A0 ) ⇒ KVAI (A). als gültig nachgewiesen wird. Die zweite Implikation ist dabei unmittelbare Konsequenz aus der Gültigkeit des Induktionsprinzps der vollständigen Induktion. Es verbleibt damit die Gültigkeit der mit (1) und (2) gekennzeichneten Implikationen nachzuweisen. Zu (1): Hier gilt: PVAI (A) ⇒ ⇒ ⇒ ⇒ ⇒ ∀ n ∈ N. ∀ m < n. A(m) ⇒ A(n) ∀ n ∈ N. A0 (n) ⇒ A(n) ∀ n ∈ N. A0 (n) ⇒ A0 (n) ∧ A(n) ∀ n ∈ N. A0 (n) ⇒ A0 (n + 1) A0 (1) ∧ ∀ n ∈ N. A0 (n) ⇒ A0 (n + 1) ⇒ PVSI (A0 ) [Def. PVAI (A)] [Def. A0 (n)] [A0 (n) ⇒ A0 (n)] [Def. A0 (n + 1)] [A0 (1) ist trivial] [Def. PVSI (A0 )] Zu (2): Hier gilt: KVSI (A0 ) ⇒ ∀ n ∈ N. A0 (n) ⇒ ∀ n ∈ N. A0 (n + 1) ⇒ ∀ n ∈ N. A(n) ⇒ KVAI (A) [Def. KVSI (A0 )] [trivial] [Def. A0 (n + 1)] [Def. KVAI (A)] 2 Mit Hilfe dieses Satzes können wir nun obige Aussage über die Fibonacci–Zahlen formal beweisen. Für die Fälle n = 0 und n = 1 ergibt sich die Aussage wie folgt: 32 KAPITEL 1. EINFÜHRUNG fib(0) = 1 ≤ 1 = 20 und fib(1) = 1 ≤ 2 = 21 . Diese beiden Fälle entsprechen dem Induktionsanfang bei der vollständigen Induktion. Für ihren Beweis steht noch keine Induktionsvoraussetzung zur Verfügung. Als Besonderheit fällt hier auf, daß der Induktionsanfang für 0 und 1, also für zwei Elemente, gemacht werden muß. Das wäre nach dem Schema der vollständigen Induktion unmöglich. Der folgende Teil des Beweises entspricht dem Induktionsschluß bei der vollständigen Induktion. Jedoch können wir nun anders als dort von n > 1 ausgehen, da der Fall n = 1 bereits als zweiter Induktionsanfang abgehandelt wurde. Dies ist wesentlich, um die Induktionsvoraussetzung an den beiden nötigen Stellen anwenden zu können. Damit ergibt sich die folgende Ungleichheitskette: fib(n) = fib(n − 2) + fib(n − 1) (Induktionsvoraussetzung für k = n − 2) ≤ 2n−2 + fib(n − 1) (Induktionsvoraussetzung für k = n − 1) ≤ 2n−2 + 2n−1 = 3 ∗ 2n−2 ≤ 4 ∗ 2n−2 = 2n . Mit Satz 1.26 folgt die Behauptung. Strukturelle Induktion Auch das verallgemeinerte Induktionsprinzip ist noch etwas schwerfällig, wenn es darum geht, Aussagen über allgemeine induktiv definierte Strukturen zu beweisen. Sei also S im folgenden eine beliebige induktiv definierte Struktur mit atomaren Objekten aus A und Operatoren aus Op. Bezeichnen wir nun die Menge aller maximalen echten Bausteine eines Elements s von S mit Komp(s), so gilt: Satz 1.27 (Strukturelle Induktion) Läßt sich eine Aussage über S für jedes Element von S aus der Gültigkeit der Aussage für alle kleineren Bausteine ableiten, dann ist sie für jedes Element von S wahr. ∀s ∈ S. ∀s0 ∈ Komp(s). A(s0 ) ⇒ A(s) ⇒ ∀ s ∈ S. A(s) . 3 Beweis Sei also A eine Aussage über S, die sich durch strukturelle Induktion beweisen läßt. 33 1.3. STRUKTUR UND INVARIANZ Da S induktiv definiert ist, läßt sich jedes Element s von S in endlich vielen Schritten aus atomaren Bausteinen zusammensetzen. Insbesondere ist die Funktion l : S → N, die jedem Element von S die minimale Anzahl der für seine Konstruktion nötigen Konstruktionsschritte zuordnet, wohldefiniert. Damit läßt sich die Aussage A über Elemente von S in eine Aussage A0 über natürliche Zahlen umwandeln: ∀n ∈ N. A0 (n) =df (∀s ∈ S. (l(s) = n) ⇒ A(s)) Offenbar genügt es nun zu zeigen, daß sich die Aussage A 0 mit verallgemeinerter Induktion beweisen läßt (basierend auf unserer Annahme, daß sich A durch strukturelle Induktion beweisen läßt). Seien also n ∈ N und s ∈ S mit l(s) = n. Wegen der allgemeinen Wahl von s genügt es nun, A(s) zu zeigen, um die Gültigkeit von A0 (n) nachweisen. Im Falle von n = 1 ist s offenbar ein atomares Objekt. Der Erfolg der strukturellen Induktion liefert damit unmittelbar die Gültigkeit von A(s). Sei also nun n > 1. Dann hat s die Gestalt f (s 1 , · · · , sk ) für geeignete si ∈ S und es gilt nach Konstruktion: ∀ 1 ≤ i ≤ k. l(si ) < n und damit nach Induktionsvoraussetzung (der verallgemeinerten Induktion): ∀1 ≤ i ≤ k. A(si ) Da aber {s1 , · · · , sk } = Komp(s) gilt, ist das gerade die Induktionsvoraussetzung, die im strukturellen Induktionsbeweis für A auftritt. Die Gültigkeit des strukturellen Induktionsbeweises liefert also unmittelbar die Gültigkeit von A(n). 2 Strukturelle Definition von Funktionen und Eigenschaften Grundlage für die strukturelle Definition von Funktionen und Eigenschaften ist die induktive Definition des zugrundeliegenden Definitionsbereichs, dessen Struktur sie folgt: Die strukturelle Definition einer Funktion f mit induktiv definiertem Definitionsbereich erfolgt (im elementaren Fall) durch 1. Festlegung von f für die atomaren Bausteine des Definitionsbereichs und 2. Angabe einer Vorschrift, wie sich die Funktionswerte komplexerer Bausteine relativ zu den Funktionswerten ihrer Komponenten berechnen. 34 KAPITEL 1. EINFÜHRUNG Folgen wir dem Format der ‘verallgemeinerten’ Induktion, so erhalten wir eine Beschreibungsform, die sich auf eine endliche Anzahl bekannter Funktionen auf dem Wertebereich stützt. Die Funktionsdefinition besteht dann aus einer Fallunterscheidung bzgl. der syntaktischen Struktur des betrachteten Elements s ∈ S. Für jeden Fall gibt es dann eine definierende Gleichung der Form: f (s) =df gcons (f (s1 ), .., f (sk )), falls s ein durch Konstruktor cons zusammengesetztes Objekt der Form s = cons(s 1 , .., sk ), (k ≥ 0) ist und gcons eine auf dem Wertebereich bekannte Funktion ist. 1.3.3 Anwendung: Semantik Boolescher Terme Nun wollen wir einem beliebigen Booleschen Term seine Bedeutung zuordnen. Dazu benötigen wir zunächst noch einige Begriffe. Definition 1.28 (Wahrheitswert, Belegung, Umgebung) Die Menge der Booleschen Wahrheitswerte {tt, ff } ( wahr“ und falsch“) wird mit ” ” B bezeichnet. Eine Funktion β : ID −→ B, die jedem Identifikator einen Wahrheitswert zuordnet, heißt Belegung oder Umgebung. Die Menge aller Belegungen wird mit ENV (für environ” ment“) bezeichnet. 3 Definition 1.29 Die Booleschen Funktionen neg : B −→ B und conj, disj : B × B −→ B sind wie folgt durch sog. Wahrheitstafeln definiert: neg tt ff ff tt conj tt ff tt tt ff ff ff ff disj tt ff tt tt tt ff tt ff 3 Durch folgende Definition wird nun Booleschen Termen eine Bedeutung (ein Wahrheitswert) zugeordnet: Definition 1.30 (Semantikfunktion) Die Semantikfunktion für Boolesche Terme ist eine Funktion [[ · ]] : BT −→ (ENV → B), die einem Booleschen Term unter Zuhilfenahme einer Belegung einen Wahrheitswert zuordnet. Sie ist wie folgt induktiv definiert: 35 1.3. STRUKTUR UND INVARIANZ • [[ true ]](β) =df tt • [[ false ]](β) =df ff • [[ id ]](β) =df β(id) für alle id ∈ ID • [[ (¬t1 ) ]](β) =df neg([[ t1 ]](β)) • [[ (t1 ∧ t2 ) ]](β) =df conj([[ t1 ]](β), [[ t2 ]](β)) • [[ (t1 ∨ t2 ) ]](β) =df disj([[ t1 ]](β), [[ t2 ]](β)) Das Semantikschema für Boolesche Terme ist somit hBT, ENV → B, [[ · ]]i, wobei ENV → B die Menge der Funktionen von ENV nach B ist. 3 Beispiel 1.31 Sei β(x) = β(y) = tt und β(z) = ff. Dann gilt: [[ (¬((x ∧ y) ∨ z)) ]](β) (Definition von [[ (¬t) ]]) = neg([[ ((x ∧ y) ∨ z) ]](β)) (Definition von [[ (t1 ∨ t2 ) ]]) = neg(disj([[ (x ∧ y) ]](β), [[ z ]](β))) (Definition von [[ (t1 ∧ t2 ) ]]) = neg(disj(conj([[ x ]](β), [[ y ]](β)), [[ z ]](β))) (Definition von [[ id ]], id ∈ ID) = neg(disj(conj(β(x), β(y)), β(z))) (Einsetzen von β ) = neg(disj(conj(tt, tt), ff )) (Definition von conj) = neg(disj(tt, ff )) (Definition von disj) = neg(tt) (Definition von neg) = ff. 3 Bevor wir die semantische Äquivalenz Boolescher Terme definieren, führen wir zunächst einige rein syntaktische Abkürzungen und Konventionen ein. Boolescher Terme in vereinfachter Syntax Konvention 1.32 Zur Vereinfachung der Schreibweise Boolescher Terme vereinbaren wir folgende Abk ürzungen (wobei n ∈ N): 36 KAPITEL 1. EINFÜHRUNG • (t1 ⇒t2 ) (lese: t1 impliziert t2“) als Abkürzung für ((¬t1 ) ∨ t2 ) (Implikation), ” • (t1 ⇔t2 ) (lese: t1 äquivalent t2“) für ((t1 ∧ t2 ) ∨ ((¬t1 ) ∧ (¬t2 ))) (Äquivalenz), ” n W ti für (...((t1 ∨ t2 ) ∨ t3 ) ∨ ... ∨ tn ) (endliche Disjunktion) und • i=1 • n V ti für (...((t1 ∧ t2 ) ∧ t3 ) ∧ ... ∧ tn ) (endliche Konjunktion). i=1 3 Konvention 1.33 Weiterhin definieren wir zur Ersparnis von Klammern eine Bindungspr äzedenz: • Der einstellige Operator ¬ bindet stärker als alle anderen Operatoren. • ∧ bindet stärker als ∨. • ∨ bindet stärker als ⇒ und ⇔. Wir verstehen aber weiterhin Terme, bei denen mithilfe dieser Regeln Klammern eingespart wurden, lediglich als Abkürzung für die entsprechenden vollständig geklammerten Terme. 3 Beispiel 1.34 In den folgenden Beispielen bezeichnet =“ die syntaktische Gleichheit auf Booleschen ” Termen. • (x ∧ y ∨ z) = ((x ∧ y) ∨ z) 6= (x ∧ (y ∨ z)) • (x ∧ y) 6= (y ∧ x) • (x ∨(y ∨ z)) 6= ((x ∨ y) ∨ z) • ((x ∨ y) ⇒ z) = ((¬(x ∨ y)) ∨ z) • (¬x ∨ y) = ((¬x) ∨ y) 6= (¬(x ∨ y)) • (x ∨ y ⇒ z) = ((x ∨ y) ⇒ z) 6= (x ∨ (y ⇒ z)) 3 Die syntaktische Gleichheit betrachtet also nicht die Bedeutung eines Termes. Insbesondere heißt das, daß beispielsweise das Kommutativ– oder das Assoziativgesetz auf der Syntax Boolescher Terme nicht gilt. Ein semantischer Gleichheitsbegriff ist Gegenstand des folgenden Abschnittes. 37 1.3. STRUKTUR UND INVARIANZ Semantische Äquivalenz Boolescher Terme Die Semantikfunktion [[ · ]], die Booleschen Termen ihre Bedeutung zuordnet, ist nicht injektiv. Das heißt, daß etwa die beiden syntaktisch verschiedenen Booleschen Terme t1 ∧ t2 und t2 ∧ t1 die gleiche Semantik besitzen. Dies liegt letztendlich an der Kommutativität von conj. Solche semantischen Gleichheiten oder Äquivalenzen wollen wir nun formal erfassen: Definition 1.35 (Semantische Äquivalenz) Zwei Boolesche Terme t1 und t2 heißen (semantisch) äquivalent, in Zeichen t1 ≡ t2 , falls gilt: ∀ β ∈ ENV. [[ t1 ]](β) = [[ t2 ]](β). 3 Beispiel 1.36 Es gelten für beliebige Boolesche Terme t, t1 , t2 , t3 die folgenden Äquivalenzen: (t ∧ t) (t ∨ t) ≡ ≡ t t (t1 ∧ t2 ) (t1 ∨ t2 ) ≡ ≡ (t2 ∧ t1 ) (t2 ∨ t1 ) ((t1 ∧ t2 ) ∧ t3 ) ((t1 ∨ t2 ) ∨ t3 ) ≡ ≡ (t1 ∧ (t2 ∧ t3 )) (t1 ∨ (t2 ∨ t3 )) (t1 ∧ (t1 ∨ t2 )) (t1 ∨ (t1 ∧ t2 )) ≡ ≡ t1 t1 (t1 ∧ (t2 ∨ t3 )) (t1 ∨ (t2 ∧ t3 )) ¬¬t ≡ ≡ ≡ ((t1 ∧ t2 ) ∨ (t1 ∧ t3 )) ((t1 ∨ t2 ) ∧ (t1 ∨ t3 )) t ¬(t1 ∧ t2 ) ¬(t1 ∨ t2 ) ≡ ≡ (¬t1 ∨ ¬t2 ) (¬t1 ∧ ¬t2 ) (true ∧ t) (f alse ∨ t) ≡ ≡ t t (t ∧ ¬t) (t ∨ ¬t) ≡ ≡ f alse true (Idempotenz) (Kommutativität) (Assoziativität) (Absorption) (Distributivität) (Doppelnegation) (deMorgansche Regeln) (Neutralität) (Negation) 38 KAPITEL 1. EINFÜHRUNG Diese Aussagen können leicht mit Hilfe von Wahrheitstafeln bewiesen werden. 3 Definition 1.37 (Äquivalenzrelation) Eine binäre Relation R über einer Menge M heißt Äquivalenzrelation, falls für beliebige x, y, z ∈ M gilt: • (x, x) ∈ R (Reflexivität), • (x, y) ∈ R impliziert (y, x) ∈ R (Symmetrie) und • (x, y) ∈ R und (y, z) ∈ R impliziert (x, z) ∈ R (Transitivit ät). 3 Äquivalenzrelationen über einer Menge M stehen in einer engen Beziehung zu sogenannten Partitionen von M . Näheres dazu: Zusatzfolien zu Äquivalenzrelationen auf der DAP1-Seite. Lemma 1.38 Die (semantische) Äquivalenz ≡ von Booleschen Termen ist eine Äquivalenzrelation. 3 Der Beweis von Lemma 1.38 ist eine Übungsaufgabe. Er ist unmittelbar anhand der Definitionen 1.35 und 1.37 zu führen. Substitutionen Gleichheitsbeweise durch algebraisches Umformen basieren auf einer Reihe von bewiesenen elementaren Gleichheiten zwischen Termen. Beweisidee ist, den einen Term solange durch ‘Ersetzen von Gleichem durch Gleiches’ umzuformen, bis der zweite Term erreicht wird. Die elementaren Gleichheiten nennt man auch Axiome und die beschriebene Vorgehensweise axiomatisch. Leider lassen sich nicht alle Gleichheitsbegriffe (z.B. nicht jede semantische Äquivalenz) axiomatisieren. Grundvoraussetzung für die Axiomatisierbarkeit eines Gleichheitsbegriffs ist, daß er eine Kongruenz definiert, d.h. daß das Prinzip ‘Ersetzen von Gleichem durch Gleiches’ gültig ist. Damit haben wir ein weiteres zentrales Designkriterium für die Definition eines Semantikschemas. Wie wir sehen werden, ist im Falle der semantischen Äquivalenz für Boolesche Terme die Welt in Ordnung, vgl. Kompositionalitätssatz 1.41. 1.3. STRUKTUR UND INVARIANZ 39 Ein weiteres wichtiges Designkriterium für Semantikschemata ist das korrekte Zusammenspiel semantischer und syntaktischer Manipulationen, oder technisch ausgedr ückt, von semantischer und syntaktischer Substitution. Auch dieses Kriterium ist für unser Semantikschema für Boolesche Terme erfüllt, vgl. Substituionslemma 1.43. Definition 1.39 (Syntaktische Substitution) Die syntaktische Substitution ist eine dreistellige Abbildung ·[·/·] : BT × BT × ID −→ BT. Intuitiv ist t1 [t2 /x] der Term, der entsteht, wenn wir in t 1 den Identifikator x an allen Stellen durch den Term t2 ersetzen (lese: t1 mit t2 für x“). ” Formal ist die Substitution für Boolesche Terme t, t1 , t2 ∈ BT und Identifikatoren x, y ∈ ID induktiv wie folgt definiert: • true[t/x] =df true • false[t/x] =df false ( t falls y = x • y[t/x] =df y sonst • (¬t1 )[t/x] =df (¬t1 [t/x]) • (t1 ∧ t2 )[t/x] =df (t1 [t/x] ∧ t2 [t/x]) • (t1 ∨ t2 )[t/x] =df (t1 [t/x] ∨ t2 [t/x]) In t1 [t2 /x] heißt t1 Kontext (von x), und jedes Auftreten von x heißt Redex (oder Anwendungsstelle). Weiterhin sei die Präzedenz von ·[·/·] größer als die aller anderen Symbole. 3 Beispiel 1.40 (x ∨ (y ∧ x))[t/x] = (t ∨ (y ∧ t)) (¬(y ∧ x))[t/x] = (¬(y ∧ t)) 3 Satz 1.41 (Kompositionalität) Seien t, t0 , t00 ∈ BT mit t0 ≡ t00 . Dann gilt t[t0 /x] ≡ t[t00 /x], d.h. man darf (simultan) Gleiches durch (semantisch) Gleiches ersetzen. 3 40 KAPITEL 1. EINFÜHRUNG Beweis Seien t, t0 , t00 ∈ BT beliebig mit t0 ≡ t00 . Nach Definition 1.35 der semantischen Äquivalenz ist für eine beliebige Belegung β ∈ ENV zu zeigen: (∗) [[ t[t0 /x] ]](β) = [[ t[t00 /x] ]](β). Wir zeigen (∗) mittels struktureller Induktion über den Termaufbau von t: 1. Fall: t ∈ {true, false, y} für y ∈ ID mit y 6= x Dann folgt (∗) wegen t[t0 /x] = t = t[t00 /x]. 2. Fall: t = x Hier folgt (∗) mit der Voraussetzung t 0 ≡ t00 : x[t0 /x] = t0 ≡ t00 = x[t00 /x]. 3. Fall: t = (¬t1 ) [[ (¬t1 )[t0 /x] ]](β) (Def. von [·/·]) = [[ (¬(t1 [t0 /x])) ]](β) (Definition von [[ · ]]) = neg([[ t1 [t0 /x] ]](β)) (Induktionsannahme) = neg([[ t1 [t00 /x] ]](β)) (Definition von [[ · ]]) = [[ (¬(t1 [t00 /x])) ]](β) (Def. von [·/·]) = [[ (¬t1 )[t00 /x] ]](β) 4. Fall: t = (t1 ∧ t2 ) [[ (t1 ∧ t2 )[t0 /x] ]](β) (Def. von [·/·]) = [[ (t1 [t0 /x] ∧ t2 [t0 /x]) ]](β) (Definition von [[ · ]]) = conj([[ t1 [t0 /x] ]](β), [[ t2 [t0 /x] ]](β)) (Induktionsannahme) = conj([[ t1 [t00 /x] ]](β), [[ t2 [t00 /x] ]](β)) (Definition von [[ · ]]) (Def. von [·/·]) = [[ (t1 [t00 /x] ∧ t2 [t00 /x]) ]](β) = [[ (t1 ∧ t2 )[t00 /x] ]](β) 5. Fall: t = (t1 ∨ t2 ) Dieser Fall ist analog zu t = (t1 ∧ t2 ) (Ersetzen von ∧“ durch ∨“ und von conj“ ” ” ” durch disj“). ” 1.3. STRUKTUR UND INVARIANZ 41 Per Induktionsprinzip folgt die Behauptung. 2 Substitutionen gibt es auch auf der Semantikseite“ für Belegungen. ” Definition 1.42 (Semantische Substitution) Die semantische Substitution ist eine dreistellige Abbildung ·{·/·} : ENV × B × ID −→ ENV definiert durch: β{b/x}(y) =df ( b β(y) falls y = x sonst 3 Semantische Substitutionen ändern also Belegungen an einer Stelle und lassen den Wert an alle anderen Stellen unverändert. Das folgende Lemma, welches die Verträglichkeit der syntaktischen Substitution mit der semantischen Substitution charakterisiert, spielt in der Logik eine zentrale Rolle: Lemma 1.43 (Substitutionslemma) Für beliebige Boolesche Terme t, t0 ∈ BT, eine beliebige Umgebung β ∈ ENV und einen beliebigen Identifikator x0 ∈ ID gilt: [[ t[t0 /x0 ] ]](β) = [[ t ]](β{[[ t0 ]](β)/x0 }), wobei [t0 /x0 ] die syntaktische Substitution und {[[ t 0 ]](β)/x0 } die semantische Substitution repräsentiert. 3 Beweis Wir beweisen die Aussage mittels struktureller Induktion über den Termaufbau von t: 1. Fall: t = true [[ true[t0 /x0 ] ]](β) = [[ true ]](β) = tt = [[ true ]](β{[[ t 0 ]](β)/x0 }). 2. Fall: t = false Dieser Fall geht analog zum 1. Fall. 3. Fall: t = x für x ∈ ID (a) x = x0 : [[ x[t0 /x] ]](β) (Definition von [·/·]) = [[ t0 ]](β) = (β{[[ t0 ]](β)/x})(x) (Definition von [[ · ]]) = [[ x ]](β{[[ t0 ]](β)/x}) 42 KAPITEL 1. EINFÜHRUNG (b) x 6= x0 : [[ x[t0 /x0 ] ]](β) = [[ x ]](β) (Definition von [[ · ]]) = β(x) = (β{[[ t0 ]](β)/x0 })(x) (Definition von [[ · ]]) = [[ x ]](β{[[ t0 ]](β)/x0 }) 4. Fall: t = (¬t1 ) [[ (¬t1 )[t0 /x0 ] ]](β) (Definition von [·/·]) = [[ ¬(t1 [t0 /x0 ]) ]](β) (Definition von [[ · ]]) = neg([[ t1 [t0 /x0 ] ]](β)) (Induktionsannahme) = neg([[ t1 ]](β{[[ t0 ]](β)/x0 })) (Definition von [[ · ]]) = [[ (¬t1 ) ]](β{[[ t0 ]](β)/x0 }) 5. Fall: t = (t1 ∧ t2 ) [[ (t1 ∧ t2 )[t0 /x0 ] ]](β) (Definition von [·/·]) = [[ (t1 [t0 /x0 ] ∧ t2 [t0 /x0 ]) ]](β) (Definition von [[ · ]]) = conj([[ t1 [t0 /x0 ] ]](β), [[ t2 [t0 /x0 ] ]](β)) (Induktionsannahme) = conj([[ t1 ]](β{[[ t0 ]](β)/x0 }), [[ t2 ]](β{[[ t0 ]](β)/x0 })) (Definition von [[ · ]]) = [[ (t1 ∧ t2 ) ]](β{[[ t0 ]](β)/x0 }) 6. Fall: t = (t1 ∨ t2 ) Dieser Fall geht analog zu t = (t1 ∧ t2 ) (Ersetzen von ∧“ durch ∨“ und von conj“ ” ” ” durch disj“). ” Per Induktionsprinzip folgt die Behauptung. Eine unmittelbare Konsequenz aus dem Substitutionslemma ist folgendes Resultat: 2 1.3. STRUKTUR UND INVARIANZ 43 Korollar 1.44 (Verträglichkeit) Für beliebige Boolesche Terme t, t0 ∈ BT, eine beliebige Umgebung β ∈ ENV und einen beliebigen Identifikator x ∈ ID gilt: [[ x ]](β) = [[ t0 ]](β) impliziert [[ t ]](β) = [[ t[t0 /x] ]](β). 3 Beweis Seien t, t0 ∈ BT, x ∈ ID und β ∈ ENV, derart daß [[ x ]](β) = [[ t 0 ]](β). Dann gilt: [[ t[t0 /x] ]](β) Lem. 1.43 = [[ t ]](β{[[ t0 ]](β)/x}) = [[ t ]](β{[[ x ]](β)/x}) = [[ t ]](β{β(x)/x}) = [[ t ]](β) 2 Kapitel 2 WHILE–Programme und ihre Semantik 2.1 Einleitung Eine Programmiersprache ist durch ihre Syntax und ihre Semantik festgelegt. Die Syntax beschreibt, wie ein formal korrekt aufgebautes Progamm auszusehen hat. Solche Programme können mit Hilfe eines Compilers in Code übersetzt werden, welcher auf einer Maschine ausführbar ist. Die Semantik legt fest, was die Ausführung eines formal korrekten Programms auf dem Rechner bewirkt. Bei imperativen Sprachen wie z.B. WHILE wird dieser Effekt in der Regel mit der durch das Programm hervorgerufenen Transformation des Speicherzustands identifiziert. Die formale Beschreibung der Syntax einer Programmiersprache z.B. mit Hilfe von BNFen ist schon seit langem selbstverständlich. Leider sieht dies in der Praxis bei Semantikbeschreibungen oft noch ganz anders aus. Informelle Semantikbeschreibungen wirken auf den ersten Blick einfacher und verständlicher. Diesem Vorteil steht aber ein Mangel an Präzision gegenüber, dessen Konsequenzen sich nicht von vorneherein abschätzen lassen. Das führt zu Unsicherheiten sowohl während des Sprachdesigns als auch während der späteren Programmierung. Im Gegensatz dazu stehen die zunächst kompliziert wirkenden formalen Beschreibungsmethoden, deren Präzision die strukturelle Entwicklung von • Programmiersprachen (fehlende Details, Mehrdeutigkeiten und Inkonsistenzen können frühzeitig erkannt werden), • Compilern (bei geeigneter Beschreibungsmethode können Compiler sogar automatisch erzeugt werden, z.B. durch attributierte Grammatiken, siehe Informatik II), und 44 2.1. EINLEITUNG 45 • Programmen (ein genaueres Verständnis der Semantik verhindert Unsicherheiten bei der Implementierung; außerdem können gemäß einem Standard, d.h. gemäß einer allgemein anerkannten Syntax– und Semantikbeschreibung entwickelte Programme problemlos portiert werden) ermöglicht. Darüber hinaus unterstützen sie Korrektheitsbeweise für • Compiler: Sowohl die Standardübersetzung“ als auch Randbedingungen für opti” mierende Programmtransformationen können verifiziert werden. • Programme: Die Bedeutung eines Programms kann anhand der Semantikbeschreibung formal hergeleitet und dann mit dem gewünschten Programmverhalten verglichen werden. Klassische formale Methoden zur Semantikbeschreibung von Programmiersprachen sind • operationelle Semantiken: Die Bedeutung eines Programms wird durch das Verhalten (die Berechnung) spezifiziert, das es auf der Maschine hervorruft (Laufzeitverhalten). Hier steht also im Vordergrund, wie ein Resultat erzielt wird. Dieses über den Effekt als Zustandstransformation hinausgehende Implementierungswissen ist von zentraler Bedeutung, wenn eine Programmiersprache mittels eines Compilers auf einem Rechner realisiert werden soll. Für die Analyse einzelner in der betrachteten Programmiersprache geschriebenen Programme ist die operationelle Semantik in der Regel zu schwerfällig. • denotationelle Semantiken: Die Bedeutung eines Programmkonstruktes wird mit Hilfe mathematischer Objekte (üblicherweise Funktionen) modelliert. Dabei steht allein der Effekt eines Programms als Zustandstransformation im Vordergrund. Von den aktuellen Implementierungsdetails wird bewußt abstrahiert. Die ‘praktische’ Relevanz der denotationellen Semantiken ist durch ihre Kompositionalit ät begründet: Die Semantik eines komplexen Programmstücks wird direkt aus den Semantiken seiner (unmittelbaren) Komponenten zusammengesetzt. Wie diese Komponenten ihre Semantik realisieren, ist dabei irrelevant. Diese laufzeitunabh ängigen Semantikspezifikationen eignen sich zur Unterstützung des Programmentwurfs nach dem Prinzip der schrittweisen Verfeinerung und zur (induktiven) Programmverifikation. • axiomatische Semantiken: Spezielle Eigenschaften des durch die Programmausführung hervorgerufenen Effekts werden durch Vor– und Nachbedingungen ausgedrückt, deren Korrektheit sich mittels des Hoare–Kalküls halbautomatisch nachweisen läßt. Das erlaubt das Vernachlässigen von im Moment als uninteressant erachteten Details. Insbesondere abstrahiert die axiomatische Semantik wie die denotationelle Semantik von Implementierungsdetails, und ähnlich wie diese eignet sie sich 46 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK auch zur Unterstützung des Programmentwurfs nach dem Prinzip der schrittweisen Verfeinerung und zur (halbautomatischen) Programmverifikation. 2.2 Syntax von WHILE In diesem Abschnitt wird die Syntax der Sprache WHILE definiert. Wir spezifizieren die (konkrete) Syntax von WHILE–Programmen mit Hilfe einer BNF. Diese Notation haben wir schon im vorangegangenen Kapitel kennengelernt. Die so spezifizierten (syntaktisch korrekten) Programmfragmente sind in fünf syntaktische Bereiche gegliedert. Diese fünf Bereiche und ihre zugehörigen Metavariablen sind: • die Zahlen Num mit Metavariable n, • die Variablen Var mit Metavariable x, • die arithmetischen Ausdrücke Aexp mit Metavariable a, • die Booleschen Ausdrücke Bexp mit Metavariable b. Wir unterscheiden explizit zwischen den auf Seite 21 eingeführten Booleschen Termen BT, die Formeln auf logischem Niveau (Meta-Level) beschreiben und Booleschen Ausdrücken Bexp, die Bedingungen innerhalb der Sprasche der WHILE-Programme modellieren. • die Anweisungen Stm mit Metavariable S. Wir verzichten hier auf detailliertere Darstellungen von Zahlen und Variablen, und betrachten diese als gegebene Objekte. Die Struktur der anderen Sprachkonstrukte ist nun induktiv durch folgende BNF gegeben: a ::= n | x | (a + a) | (a ∗ a) b ::= true | false | (a == a) | (a < a) | (a <= a) | (!b) | (b && b) | (b || b) S ::= x = a | skip | S; S | if (b) {S} else {S} | while (b) {S} Beispiel 2.1 Wie die syntaktische Korrektheit eines While–Programms mit Hilfe der BNF-Definition nachgewiesen werden kann, wird am Beispielprogramm y = 1; while (1 < x) {y = y ∗ x; x = x − 1 } aus Abbildung 2.1 vorgeführt. Bei der Interpretation von 1 ∈ Num durch 1 ∈ Z berechnet das Beispielprogramm die Fakultät des Wertes von x. Ein While–Programm ist syntaktisch korrekt, wenn es sich 2.2. SYNTAX VON WHILE 47 S S;S y=a n while (b) {S} (a<a) n x S;S y=a x=a (a*a) (a−a) xy x n Abbildung 2.1: Ableitung eines While–Programms aus der BNF • auf das Axiom S reduzieren läßt, d.h. wenn es sich durch sukzessive ‘Termersetzung’ entlang der von rechts nach links gelesenen BNF–Regeln in das Symbol S überführen läßt, oder äquivalent dazu, wenn es sich • aus dem Axiom ableiten läßt, d.h. wenn es durch sukzessive ‘Termersetzung’ entlang der von links nach rechts gelesenen BNF-Regeln aus dem Symbol S erzeugen läßt. 3 Abschließend wollen wir noch auf eine grafische Beschreibungsmethode für die Syntax von Programmiersprachen eingehen, die sog. Syntaxdiagramme. Auch diese zu den BNFSpezifiaktionen äquivalente Methode demonstrieren wir anhand von WHILE. Grundbausteine für Syntaxdiagramme sind durch Pfeile verbundene, beschriftete rechteckige und ovale Kästchen. Diese sind so anzuordnen, daß ein gerichteter Graph mit genau einem Eingangs- und einem Ausgangspfeil entsteht. Diese Struktur entspricht der rechten Seite einer BNF–Gleichung. Das linksseitig auftretende Nichtterminalsymbol der BNF–Gleichungen spiegelt sich darin wider, daß Syntaxdiagramme benannt sind. Auf diese Weise kann in den verschiedenen Syntaxdiagrammen, die eine Syntaxdefinition ausmachen, wechselseitig aufeinander Bezug genommen werden: Innerhalb der rechteckigen Kästchen der Syntaxdiagramme stehen Namen, die das Syntaxdiagramm bezeichnen, das während eines Ableitungsschritts anstelle des Kästchens einzukopieren ist. Bezeichner in den ovalen Kästchen sind Programmtexte. Sie sind wie die Terminalsymbole der BNF einfach an den betreffenden Stellen einzusetzen. 48 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Um nun ein syntaktisch korrektes Programm zu erhalten, durchläuft man das Syntaxdiagramm den Pfeilen folgend vom Eingangspfeil zum Ausgangspfeil, wobei man rechteckige Kästchen durch ihre definierenden Syntaxdiagramme ersetzt und alle Texte in ovalen Kästchen sukzessive notiert. Die Bedeutung eines Syntaxdiagramms im Rahmen einer umfassenden Syntaxdefintion ist also die Menge aller Terminalsymbolfolgen, denen ein Pfad obiger Bauart entspricht. Wie BNF–Definitionen können auch Syntaxdiagramme rekursiv sein, d.h. in dem mit X bezeichneten Diagramm kann X selbst direkt oder indirekt (d.h. erst nach dem Einkopieren anderer Syntaxdiagramme) vorkommen. Das ändert aber nichts an den oben genannten Prinzipien. Die Syntax von WHILE läßt sich mit Hilfe rekursiver Syntaxdiagramme spezifizieren (vgl. Abbildung 2.2), wobei wir diesmal auf die Spezifikation der Syntax arithmetischer und Boolescher Ausdrücke verzichtet haben (vgl. Übung). Iteration versus Rekursion: In manchen Fällen kann Rekursion durch eine äquivalente iterative Lösung (d.h. durch ein Syntaxdiagramm mit einer Schleife) ersetzt werden. Beispielsweise bezeichnen die Diagramme aus den Abbildungen 2.3 und 2.4 dieselben Ziffernfolgen. Immer geht dies jedoch nicht, wie man sich an dem Beispiel beliebig geschachtelter, geklammerter arithmetischer Ausdrücke klarmachen kann. Die Problematik wird an folgendem auf die Essenz vereinfachten (rekursiven) Syntaxdiagramm A aus Abbildung 2.5 klarer, das kein iteratives Äquivalent besitzt. Diese Überlegungen werden in der Vorlesung “Grundzüge Theoretischer Informatik’ ausführlich diskutiert. 2.3 2.3.1 Semantik von WHILE Semantik von arithmetischen und Booleschen Ausdrücken Die Semantik eines Ausdrucks soll seinen Wert im üblichen Sinne widerspiegeln. Dabei ist zu beachten, daß der Wert eines Ausdrucks von den Wertebindungen der in ihm vorkommenden Variablen (Identifikatoren) abhängt. Bemerkung 2.2 Variablenidentifikatoren haben einen Doppelcharakter: als Variable“ sind sie an einen ” Wert gebunden und als Ausdruck“ haben sie einen Wert. Dieser Doppelcharakter ist ” zentral für die Interpretation von Zuweisungen: rechtsseitige Vorkommen bezeichnen einen Ausdruck und linksseitige eine Variable. 3 2.3. SEMANTIK VON WHILE 49 while−Programme (WP) (Wert) Zuweisung skip−Anweisung (sequentielle) Komposition if−then−else−Anweisung while−Anweisung (Wert) Zuweisung = Variable (arithm.) Ausdruck skip−Anweisung skip (sequentielle) Komposition WP ; WP if−then−else−Anweisung if { ( WP Boolescher Term } else ) { WP ) { } while−Anweisung while ( Boolescher Term Abbildung 2.2: Syntaxdiagramme für die WHILE–Sprache Ziffernfolge Ziffer Abbildung 2.3: Syntaxdiagramm ohne Rekursion WP } 50 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Ziffernfolge 1 Ziffernfolge 1 Ziffer Abbildung 2.4: Syntaxdiagramm mit Rekursion A a A b Abbildung 2.5: Beispiel Ein Speicherzustand, im folgenden kurz Zustand genannt, ist ein Element der Menge Σ =df {σ | σ : Var −→ Z}, wobei Var die Menge aller Variablen bezeichnet. Eine ähnliche Konstruktion hatten wir schon bei der Definition der Bedeutung Boolescher Terme als ‘Umgebung’ kennengelernt. Hier gehen wir nun davon aus, daß den Variablen ganze Zahlen, d.h. Elemente von Z, zugeordnet werden. Weiterhin nehmen wir an, daß alle Variablen in jedem Zustand einen definierten Wert besitzen. Die Bedeutung arithmetischer und Boolescher Ausdrücke wird jeweils durch eine Interpretationsfunktion (vgl. Boolesche Terme) definiert: [[ · ]]A : Aexp −→ (Σ → Z) und [[ · ]]B : Bexp −→ (Σ → B), wobei B wieder die Menge {tt, ff } bezeichne. Um [[ · ]]A vollständig induktiv definieren zu können, benötigen wir noch eine weitere Interpretationsfunktion [[ · ]]N : Num −→ Z für Zahlen, die den syntaktischen Objekten in Num semantische Objekte, hier ganze Zahlen, zuordnet. Weiter gehen wir im folgenden davon aus, daß Zahlen im Dezimalsystem dargestellt werden. Damit können wir [[ · ]]A induktiv definieren: • [[ n ]]A (σ) = [[ n ]]N ( σ bezeichne einen Zustand) • [[ x ]]A (σ) = σ(x) • [[ a1 + a2 ]]A (σ) = plus([[ a1 ]]A (σ), [[ a2 ]]A (σ)) • [[ a1 ∗ a2 ]]A (σ) = mul([[ a1 ]]A (σ), [[ a2 ]]A (σ)) 2.3. SEMANTIK VON WHILE 51 • [[ a1 − a2 ]]A (σ) = minus([[ a1 ]]A (σ), [[ a2 ]]A (σ)) Hierbei stehen plus“, mul“ und minus“ für die binäre semantische Addition, Multipli” ” ” kation und Subtraktion, d.h. für die binäre Addition, Multiplikation bzw. Subtraktion in Z. Der besseren Lesbarkeit halber werden wir später wieder +“, ∗“ bzw. −“ schrei” ” ” ben, d.h. syntaktische und semantische Operatoren gleich bezeichnen. Beachte, daß die Semantik von Ausdrücken induktiv entlang des syntaktischen Aufbaus definiert ist. Das ist die Voraussetzung für spätere Beweise durch strukturelle Induktion. Beispiel 2.3 Angenommen σ(x) = 3. Dann gilt: [[ x + 1 ]]A (σ) = [[ x ]]A (σ) + [[ 1 ]]A (σ) = σ(x) + [[ 1 ]]N = 3 + 1 = 4 3 Analog zu [[ · ]]A definieren wir [[ · ]]B . Dazu benötigen wir wie bei der Definition der Interpretationsfunktion für Boolesche Terme die Hilfsfunktionen neg, conj und disj. • [[ true ]]B (σ) =df tt • [[ false ]]B (σ) =df ff ( • [[ a1 == a2 ]]B (σ) =df • [[ a1 < a2 ]]B (σ) =df • [[ a1 <= a2 ]]B (σ) =df ( tt ff tt ff ( falls eq([[ a1 ]]A (σ), [[ a2 ]]A (σ)) sonst falls less([[ a1 ]]A (σ), [[ a2 ]]A (σ)) sonst tt ff falls leq([[ a1 ]]A (σ), [[ a2 ]]A (σ)) sonst • [[ !b ]]B (σ) =df neg([[ b ]]B (σ)) • [[ b1 && b2 ]]B (σ) =df conj([[ b1 ]]B (σ), [[ b2 ]]B (σ)) • [[ b1 || b2 ]]B (σ) =df disj([[ b1 ]]B (σ), [[ b2 ]]B (σ)) In dieser Definition steht eq“ für die semantische Gleichheit in Z, less“ für die seman” ” tische kleiner“–Relation in Z und leq“ für die semantische kleiner gleich“–Relation ” ” ” in Z. Im folgenden werden wir wie üblich =“, <“, ≤“ etc. schreiben. Beachte, daß ” ” ” wir das Symbol “<” sowohl für die semantische Relation als auch für das syntaktische Relationszeichen verwenden. 52 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Die Menge der freien Variablen ist in der Regel für die Semantik von Ausdrücken von entscheidender Bedeutung. Bei WHILE–Programmen haben wir allerdings eine besondere Situation: alle Variablenvorkommen sind frei. Das ist aber z.B. im Hoare–Kalk ül anders (vgl. Abschnitt 2.3.4). Deshalb definieren wir die Semantik von Ausdrücken hier in der vollen Allgemeinheit. Die Menge der freien Variablen kann formal durch die Funktion FV : Aexp ∪ Bexp −→ P(Var), wobei P(Var) die Potenzmenge der Variablenmenge Var bezeichne, definiert werden: • FV(n) =df ∅ • FV(x) =df {x} • FV(a1 + a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(a1 ∗ a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(a1 − a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(true) =df ∅ • FV(false) =df ∅ • FV(a1 == a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(a1 < a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(a1 <= a2 ) =df FV(a1 ) ∪ FV(a2 ) • FV(!b) =df FV(b) • FV(b1 &&b2 ) =df FV(b1 ) ∪ FV(b2 ) Lemma 2.4 Seien σ, σ 0 ∈ Σ mit σ(x) = σ 0 (x) für alle x ∈ FV(a) bzw. x ∈ FV(b) . Dann gilt: [[ a ]]A (σ) = [[ a ]]A (σ 0 ) bzw. [[ b ]]B (σ) = [[ b ]]B (σ 0 ) 3 Beweis Seien σ, σ 0 ∈ Σ. Wir beweisen die folgende Aussage durch strukturelle Induktion über den Aufbau des Ausdrucks a: ∀ a ∈ Aexp. (∀ x ∈ FV(a). σ(x) = σ 0 (x)) ⇒ [[ a ]]A (σ) = [[ a ]]A (σ 0 ) Sei a ∈ Aexp. Für alle Teilterme a0 von a gelte die Behauptung, d.h.: (∀ x ∈ FV(a0 ). σ(x) = σ 0 (x)) ⇒ [[ a0 ]]A (σ) = [[ a0 ]]A (σ 0 ) (IA) Weiterhin gelte: ∀ x ∈ FV(a). σ(x) = σ 0 (x). (∗) 2.3. SEMANTIK VON WHILE 53 1. a = n ∈ Num. Dann gilt: [[ a ]]A (σ) = [[ n ]]A (σ) = [[ n ]]N = [[ n ]]A (σ 0 ) = [[ a ]]A (σ 0 ). a = x ∈ Var. Dann ist x ∈ FV(a) und es gilt σ(x) = σ 0 (x) nach Annahme (*). Somit: [[ a ]]A (σ) = [[ x ]]A (σ) = σ(x) = σ 0 (x) = [[ x ]]A (σ 0 ) = [[ a ]]A (σ 0 ). 2. a = a1 + a2 ∈ Aexp. Dann erhalten wir zunächst: [[ a ]]A (σ) = [[ a1 + a2 ]]A (σ) = [[ a1 ]]A (σ) + [[ a2 ]]A (σ) und [[ a ]]A (σ 0 ) = [[ a1 + a2 ]]A (σ 0 ) = [[ a1 ]]A (σ 0 ) + [[ a2 ]]A (σ 0 ). Da a1 und a2 unmittelbare Teilausdrücke von a1 + a2 sind und offenbar FV(a1 ) ⊆ FV(a) sowie FV(a2 ) ⊆ FV(a) gilt, folgt mit Hilfe der Induktionsannahme (IA): [[ ai ]]A (σ) = [[ ai ]]A (σ 0 ) für i ∈ {1, 2} und wegen der Wohldefiniertheit von + die Behauptung. Für Ausdrücke a1 ∗ a2 und a1 − a2 ist die Argumentation analog. Per Induktionsprinzip folgt die Behauptung. Der Beweis der Aussage für Boolesche Ausdrücke geht analog. 2 Der Satz der Kompositionalität und das Substitutionslemma, die wir im Zusammenhang mit Booleschen Termen kennengelernt haben, können auch für Boolesche und arithmetische Ausdrücke formuliert werden. Wir wollen dies hier exemplarisch für die arithmetischen Ausdrücke durchführen. Die syntaktische Substitution ·[·/·] : Aexp × Aexp × Var −→ Aexp für arithmetische Ausdrücke ist analog zur syntaktischen Substitution für Boolesche Terme definiert: Für beliebige arithmetische Ausdrücke a, a0 ∈ Aexp und eine beliebigen Variable x ∈ Var entsteht a[a0 /x] durch (simultanes) Ersetzen aller freier Vorkommen von x in a durch a 0 . Satz 2.5 (Kompositionalität) Seien a, a0 , a00 ∈ Aexp mit [[ a0 ]]A = [[ a00 ]]A und x ∈ Var beliebig. Dann gilt [[ a[a0 /x] ]]A = [[ a[a00 /x] ]]A , 54 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK d.h. für jeden Zustand σ ∈ Σ gilt: [[ a[a0 /x] ]]A (σ) = [[ a[a00 /x] ]]A (σ). 3 Lemma 2.6 (Substitutionslemma) Für beliebige arithmetische Ausdrücke a, a0 ∈ Aexp und jeden Zustand σ ∈ Σ gilt: [[ a[a0 /x] ]]A (σ) = [[ a ]]A (σ{[[ a0 ]]A (σ)/x}). 3 Sowohl der Kompositionalitätssatz als auch das Substitutionslemma lassen sich wieder durch strukturelle Induktion (über den induktiven Aufbau arithmetischer Ausdrücke) beweisen (siehe Übungen). Semantik von Anweisungen Ziel einer semantischen Modellierung ist eine präzise Erfassung des Effekts, den ein Programm in seiner Programmierumgebung hat. Üblicherweise beschränkt man sich dabei auf das zugehörige Ein–/Ausgabeverhalten. Wir werden sehen, daß der Effekt bei sequentiellen, imperativen Programmen mit Hilfe von Ein–/Ausgabefunktionen beschrieben werden kann. Die folgenden drei Abschnitte behandeln drei Standardansätze einer solchen Beschreibung, die operationelle, die denotationelle und die axiomatische Semantik. Allen drei Ansätzen liegt im wesentlichen die gleiche Beschreibung der Bedeutung elementarer Anweisungen (hier nur die skip–Anweisung und Zuweisungen) zugrunde. Der Unterschied besteht lediglich in dem ‘Mechanismus der Globalisierung’ auf ganze Programme. Tatsächlich sind die drei Ansätze in dem Sinne konsistent, daß sie Programmen dieselben Effekte zuordnen. 2.3.2 Operationelle Semantik Operationelle Semantiken globalisieren ‘argumentweise’, genau der intendierten Berechnung folgend: Zu gegebenen Eingabewerten wird die zugehörige Berechnungsfolge angegeben. Das kann zum einen mit direktem Bezug auf eine abstrakte Maschine, zum anderen aber auch abstrakter auf der Basis sogenannter Berechnungsfolgen geschehen, die den Berechnungsvorgang als Folge elementarer Speicherzustandsübergänge beschreiben. Wir folgen hier der zweiten Variante und entwickeln die strukturierte operationelle Semantik im Sinne von G. Plotkin. 2.3. SEMANTIK VON WHILE 55 Grundlegende Idee der strukturierten operationellen Semantik ist die sukzessive Transformation von Konfigurationen gemäß der Effekte elementarer Anweisungen. Es gibt zwei Typen von Konfigurationen: • hS, σi steht für die Konfiguration einer Programmausführung, in der noch das Programmfragment S auf den Zwischenzustand σ anzuwenden ist. • σ ist eine finale Konfiguration und charakterisiert das Resultat nach der Terminierung einer Berechnung. Ein Ableitungsschritt in der strukturierten operationellen Semantik hat die Form hS, σi ⇒ γ mit γ ∈ (Stm × Σ) ∪ Σ (γ ist also eine Konfiguration von einem der beiden oben angegebenen Typen). Im Falle γ ∈ Stm × Σ ist die Berechnung noch nicht abgeschlossen. γ beschreibt dann die noch verbleibenden Berechnungen. Im anderen Falle ist die Berechnung terminiert. Die vollständige Definition der möglichen Ableitungsschritte ist durch die nachstehenden Regeln gegeben. Dabei hat das Regelformat Prämisse Nebenbedingung Konklusion die folgende Bedeutung: Die Regel Name“ ist anwendbar, wenn die Prämisse und die ” Nebenbedingungen erfüllt sind. In diesem Fall kann der in der Konklusion beschriebene Schritt ausgeführt werden. Das Zeichen –“ steht für die leere Prämisse, die immer erfüllt ” ist. Regeln mit leerer Prämisse nennt man auch Axiome. [Name] [asssos ] [skipsos ] − hx = a, σi ⇒ σ{[[ a ]]A (σ)/x} − hskip, σi ⇒ σ [comp1sos ] hS1 , σi ⇒ hS10 , σ 0 i hS1 ; S2 , σi ⇒ hS10 ; S2 , σ 0 i [comp2sos ] hS1 , σi ⇒ σ 0 hS1 ; S2 , σi ⇒ hS2 , σ 0 i [iftt sos ] − [[ b ]]B (σ) = tt hif (b) {S1 } else {S2 }, σi ⇒ hS1 , σi 56 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK [ifffsos ] [whilesos ] − [[ b ]]B (σ) = ff hif (b) {S1 } else {S2 }, σi ⇒ hS2 , σi − hwhile (b) {S}, σi ⇒ hif (b) {S; while (b) {S}} else {skip}, σi Durch die Hintereinanderausführung von ‘zueinander passenden’ Ableitungsschritten entsteht eine sog. Berechnungsfolge. Definition 2.7 (Berechnungsfolgen) Eine Berechnungsfolge zu einer Anweisung S im Startzustand σ ist: • eine endliche Folge γ0 , . . . , γk von Konfigurationen mit γ0 = hS, σi und γi ⇒ γi+1 für alle i ∈ {0, . . . , k − 1}. • eine unendliche Folge γ0 , γ1 , ... von Konfigurationen mit γ0 = hS, σi und γi ⇒ γi+1 für alle i ∈ N. Wir schreiben γ ⇒i γ 0 , falls es eine Berechnungsfolge der Länge i von γ nach γ 0 gibt und γ ⇒∗ γ 0 , falls es eine endliche Berechnungsfolge von γ nach γ 0 gibt. Weiter heißt eine maximale (d.h. nicht weiter verlängerbare) Berechnungsfolge terminierend, falls sie endlich ist, und divergierend, falls sie unendlich ist. 3 Wichtig: Der Berechnungscharakter der While–Schleife“ spiegelt sich bei der struktu” rierten operationellen Semantik in Iterationen entlang der Berechnungsfolgen wider. Beispiel 2.8 Sei σ ∈ Σ ein beliebiger Zustand. Für n, m ∈ Z sei σnm der Zustand σ{n/x}{m/y}. Wir betrachten für das Fakultätprogramm S 00 = x−1 } y = 1; while (1 < x) { y = y ∗ x ; x | {z } | {z } | {z } | S2 S1 b | {z S0 {z S } } und den Anfangszustand σ30 die Berechnungsfolge gemäß der Regeln zur strukturierten 2.3. SEMANTIK VON WHILE 57 operationellen Semantik: hy = 1; while (1 < x) {y = y ∗ x; x = x − 1}, σ 30 i (a) =⇒ hwhile (1 < x) {y = y ∗ x; x = x − 1}, σ31 i (b) =⇒ hif (1 < x) {y = y ∗ x; x = x − 1; S0 } else {skip}, σ31 i (c) =⇒ hy = y ∗ x; x = x − 1; S0 , σ31 i (d) =⇒ hx = x − 1; S0 , σ33 i (e) =⇒ hwhile (1 < x) {y = y ∗ x; x = x − 1}, σ23 i (f) =⇒ hif (1 < x) {y = y ∗ x; x = x − 1; S0 } else {skip}, σ23 i (g) =⇒ hy = y ∗ x; x = x − 1; S0 , σ23 i (h) =⇒ hx = x − 1; S0 , σ26 i (i) =⇒ hwhile (1 < x) {y = y ∗ x; x = x − 1}, σ16 i (j) =⇒ hif (1 < x) {y = y ∗ x; x = x − 1; S0 } else {skip}, σ16 i (k) =⇒ hskip, σ16 i (l) =⇒ σ16 Die Schritte (b), (f) und (j) sind Anwendungen von [while sos ], sie haben keine Prämisse und brauchen deshalb nicht weiter gerechtfertigt zu werden; ebenso der [skip sos ]-Schritt ff (l). Bei (c) und (g) wird [iftt sos ] und bei (k) [ifsos ] benutzt, auch diese Regeln haben keine Prämisse, jedoch muß die jeweilige Nebenbedingung den then- bzw. else-Schritt rechtfertigen. Besondere Beachtung verdienen die [comp 1sos ]-Regelanwendungen, deren Rechtfertigung zusammen mit den noch offenen Begründungen nachstehend aufgeführt sind. 58 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK (a) : (c) [comp2sos ] [asssos ] − hy = 1, σ3 0i ⇒ σ31 hy = 1; S0 , σ3 0i ⇒ hS 0 , σ31 i : [[ 1 < x ]]B (σ31 ) = tt da [[ 1 ]]A (σ31 ) < [[ x ]]A (σ31 ) ⇔ [[ 1 ]]N < σ31 (x) ⇔ 1 < 3 asssos ] [ (d) : [comp1sos ] (e) : [comp 2sos ] − hy = y ∗ x, σ31 i ⇒ σ33 hy = y ∗ x; x = x − 1, σ31 i ⇒ hx = x − 1, σ33 i hy = y ∗ x; x = x − 1; S0 , σ31 i ⇒ hx = x − 1; S0 , σ33 i [comp2 sos ] [asssos ] − hx = x − 1, σ33 i ⇒ σ23 hx = x − 1; S0 , σ33 i ⇒ hS 0 , σ23 i (g) : [[ 1 < x ]]B (σ23 ) = tt da [[ 1 ]]A (σ23 ) < [[ x ]]A (σ23 ) ⇔ [[ 1 ]]N < σ23 (x) ⇔ 1 < 2 asssos ] [ (h) : [comp 1sos ] (i) : [comp 2sos ] − hy = y ∗ x, σ23 i ⇒ σ26 hy = y ∗ x; x = x − 1, σ23 i ⇒ hx = x − 1, σ26 i hy = y ∗ x; x = x − 1; S0 , σ23 i ⇒ hx = x − 1; S0 , σ26 i [comp2 sos ] [asssos ] − hx = x − 1, σ26 i ⇒ σ16 hx = x − 1; S0 , σ26 i ⇒ hS 0 , σ16 i (k) : [[ 1 < x ]]B (σ16 ) = ff da nicht gilt: [[ 1 ]]A (σ16 ) < [[ x ]]A (σ16 ) ⇔ [[ 1 ]]N < σ16 (x) ⇔ 1 < 1 3 Eigenschaften der strukturierten operationellen Semantik Das folgende Lemma zeigt, wie sich die Komposition von Programmen in den Berechnungsfolgen widerspiegelt. Lemma 2.9 Sei hS1 ; S2 , σi ⇒k σ 00 . Dann gibt es einen Zustand σ 0 und natürliche Zahlen k1 und k2 , so daß gilt: • hS1 , σi ⇒k1 σ 0 , • hS2 , σ 0 i ⇒k2 σ 00 und • k = k 1 + k2 . 2.3. SEMANTIK VON WHILE Beweis Beweis durch Induktion nach der Länge der Berechnungsfolgen, d.h. hier 59 3 1. Beweis der Eigenschaft für alle Berechnungsfolgen der Länge Null (Induktionsanfang). 2. Beweis der Eigenschaft für alle Berechnungsfolgen der Länge k + 1 unter der Annahme, daß sie für alle Berechnungsfolgen kleinerer Längen richtig ist (Induktionsschritt). Beweis des Induktionsanfangs: Im Falle k = 0 ist die Voraussetzung unerfüllbar, also die Behauptung trivial. Beweis des Induktionsschritts: Sei nun hS1 ; S2 , σi ⇒k+1 σ 00 . Dann sind zwei Fälle zu unterscheiden: 1. Die Regel [comp1sos ] wurde zur Rechtfertigung des ersten Berechnungsschrittes angewandt. In diesem Fall läßt sich hS1 ; S2 , σi ⇒ hS10 ; S2 , σ 000 i aus hS1 , σi ⇒ hS10 , σ 000 i ableiten. Wenden wir nun die Induktionsvoraussetzung auf hS 10 ; S2 , σ 000 i ⇒k σ 00 an, so folgt die Existenz eines Zustands σ 0 und zweier Zahlen k1 , k2 ∈ N mit hS10 , σ 000 i ⇒k1 σ 0 , hS2 , σ 0 i ⇒k2 σ 00 und k = k1 + k2 . Das liefert aber wie gewünscht: hS1 , σi ⇒k1 +1 σ 0 , hS2 , σ 0 i ⇒k2 σ 00 und k + 1 = (k1 + 1) + k2 . 2. Nur die Regel [comp2sos ] wurde zur Rechtfertigung des ersten Berechnungsschrittes angewandt. Dann gilt: hS1 ; S2 , σi ⇒ hS2 , σ 0 i für einen geeigneten Zustand σ 0 . Also gilt die Behauptung für k1 = 1 und k2 = k. 2 Theorem 2.10 (Determiniertheit) Die strukturierte operationelle Semantik ist deterministisch, d.h. hS, σi ⇒k1 σ 0 und hS, σi ⇒k2 σ 00 impliziert σ 0 = σ 00 and k1 = k2 . 3 60 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Der Beweis kann durch verallgemeinerte Induktion über die Länge der Berechnungsfolge k1 geführt werden. Im Induktionsschritt sind abhängig von dem Aufbau von S verschiedene Möglichkeiten eines ersten Schrittes zu untersuchen. Falls S keine sequentielle Komposition ist, ist die Argumentation einfach, da der erste Schritt eindeutig festgelegt ist. Ansonsten betrachten wir S vollständigt zerlegt in seine sequentiellen Komponenten, also in einer Form S1 ; . . . ; Sn , auf die sukkzessive Lemma 2.9 angewendet werden kann. Mittels wiederholter Anwendung der Induktionsvoraussetzung kann schließlich die Eindeutigkeit der Berechnungsfolge gezeigt werden. Die exakte Durchführung dieses Beweises sei dem Leser als Übungsaufgabe überlassen. Damit können wir jetzt das Semantikschema hStm, Σ− →Σ, [[ · ]] sos i zur strukturierten operationellen Semantik durch Angabe des folgenden Funktionals vervollständigen.1 Definition 2.11 (Operationelles Semantikfunktional) Die Funktion [[ · ]]sos : Stm −→ (Σ− →Σ), die durch ( σ0 falls hS, σi ⇒∗ σ 0 [[ S ]]sos (σ) = undefiniert sonst definiert ist, heißt semantisches Funktional zur strukturierten operationellen Semantik. 3 Das semantische Funktional zur strukturierten operationellen Semantik weist jeder Anweisung eine partielle Funtion von der Menge der Zustände in die Menge der Zustaände, eine sogenannte partielle Zustandstransformation, zu. Bemerkung 2.12 Die Wohldefiniertheit des semantischen Funktionals [[ · ]] sos folgt unmittelbar aus Theorem 2.10. 3 Die Rechtfertigung des Begriffs strukturierte operationelle Semantik liefert der folgende Satz: Satz 2.13 (Kompositionalität der strukturierten operationellen Semantik) Sei [[ Si ]]sos = [[ Si0 ]]sos für beliebige Si , Si0 ∈ Stm und i ∈ {1, 2}. Dann gilt: 1. [[ S1 ; S2 ]]sos = [[ S10 ; S20 ]]sos 2. [[ if (b) {S1 } else {S2 } ]]sos = [[ if (b) {S10 } else {S20 } ]]sos 3. [[ while (b) {S1 } ]]sos = [[ while (b) {S10 } ]]sos 3 1 Dabei bezeichnet ’− →’ eine partiell definierte Funktion, also eine, die nicht an jeder Argumentposition einen definierten Funktionswert besitzen muss. 2.3. SEMANTIK VON WHILE 2.3.3 61 Denotationelle Semantik In diesem Abschnitt soll der Effekt von Programmen losgelöst von deren konkretem Laufzeitverhalten definiert werden. D.h. anders als im operationellen Fall, wo das zugehörige Semantikschema entlang der schrittweisen Programmausführung definiert wurde, soll hier eine der syntaktischen Struktur des Programms folgende Definition entwickelt werden. Diesem Ansatz liegt die Beobachtung zugrunde, daß das semantische Funktional [[ · ]] sos nicht nur so abstrakt wie möglich (d.h. genau auf die Essenz (Effekt) beschränkt ist – eine typische Eigenschaft operationeller Semantiken), sondern gleichzeitig auch kompositionell ist (siehe Satz 2.13). Kern der denotationellen Semantik ist also die direkte strukturierte Definition eines Semantikfunktionals [[ · ]]ds : Stm −→ (Σ− →Σ). Definieren wir die semantische bedingte Anweisung“ durch ” cond : (Σ → B) × (Σ− →Σ)2 −→ (Σ− →Σ), mittels cond(p, g1 , g2 )(σ) = ( g1 (σ) g2 (σ) falls p(σ) = tt . sonst so kann [[ · ]]ds induktiv wie folgt definiert werden: • [[ x = a ]]ds (σ) =df σ{[[ a ]]A (σ)/x} • [[ skip ]]ds =df id (Identität auf den Zustandstransformationen) • [[ S1 ; S2 ]]ds =df [[ S2 ]]ds ◦ [[ S1 ]]ds • [[ if (b) {S1 } else {S2 } ]]ds =df cond([[ b ]]B , [[ S1 ]]ds , [[ S2 ]]ds ) • [[ while (b) {S} ]]ds =df fix Φ, wobei Φ(g) =df cond([[ b ]]B , g ◦ [[ S ]]ds , id) Die ersten vier definierenden Gleichnungen spiegeln direkt die übliche (operationelle) Intuition wider. Interessant und neu ist die fünfte Gleichung, deren neue ‘Ingredienz’ fix im folgenden motiviert, präzisiert und fundiert werden soll. Hier steht fix für Fixpunkt“. Die Betrachtung eines Fixpunkts in diesem Zusammenhang ” ist durch folgende Reduktion“ von while“ auf if () {} else {}“ mit Hilfe des bereits ” ” ” ‘etablierten’ Teils der Definition von [[ · ]] ds motiviert: [[ while (b) {S} ]]ds = cond([[ b ]]B , [[ while (b) {S} ]]ds ◦ [[ S ]]ds , id) 62 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK [[ while (b) {S} ]]ds muß also ein Fixpunkt des Funktionals Φ : (Σ− →Σ) −→ (Σ− →Σ) sein, d.h. Φ([[ while (b) {S} ]]ds ) = [[ while (b) {S} ]]ds . Leider reicht diese Erkenntnis noch nicht aus, um ein wohldefiniertes Funktional zu erhalten, denn • es kann mehrere Fixpunkte geben, z.B. hat das Funktional Φ 1 (g) = g (Identität) jede Zustandstransformation g 0 : Σ− →Σ als Fixpunkt. • es braucht gar keine Fixpunkte zu geben, z.B. hat ( g1 falls g = g2 Φ2 (g) = g2 sonst für g1 6= g2 keine Fixpunkte. Dieses Dilemma wird wie folgt behoben: • die Mehrdeutigkeit durch die Auswahl eines speziellen Fixpunktes. • die Existenz durch strukturelle Einschränkungen bei der Definition des Funktionals Φ (vgl. elementare Fixpunkttheorie). Um zu erklären, was den ‘intendierten’ Fixpunkt fix Φ gegenüber anderen Fixpunkten von Φ auszeichnet, betrachten wir zunächst einen beliebigen Fixpunkt g0 von Φ. Bei gegebenem Anfangszustand σ0 können bei einer while–Schleife while (b) {S} drei Fälle auftreten: 1. Es gibt Zustände σ1 , . . . , σn , n ≥ 0, derart, daß ( tt falls i ∈ {0, ..., n − 1} [[ b ]]B (σi ) = ff falls i = n und [[ S ]]ds (σi ) = σi+1 für i ∈ {0, . . . , n − 1} Das entspricht dem erfolgreichen Terminieren der while–Schleife nach n − 1 Schleifendurchläufen. 2. Es gibt Zustände σ0 , . . . , σn , n ≥ 0, derart, daß [[ b ]] B (σi ) = tt für alle i ∈ {0, . . . , n} und ( σi+1 falls i ∈ {0, ..., n − 1} [[ S ]]ds (σi ) = undefiniert sonst 2.3. SEMANTIK VON WHILE 63 Das entspricht der Situation, in der der Schleifenrumpf in der n–ten Iteration divergiert. Damit dies geschehen kann, muß S noch eine weitere while–Schleife enthalten, die nun divergiert. Die Divergenz findet also nicht auf dem betrachteten Schleifenniveau statt. 3. Es gibt Zustände σ1 , σ2 , σ3 , ... derart, daß [[ b ]] B (σi ) = tt für alle i ∈ N und [[ S ]]ds (σi ) = σi+1 für alle i ∈ N Das entspricht der Situation, in der die betrachtete Schleife divergiert. Im ersten Fall erhalten wir nach Definition von Φ für i ∈ {0, . . . , n − 1} und einen beliebigen Fixpunkt g0 von Φ: (Φ(g0 ))(σi ) = g0 ◦ [[ S ]]ds (σi ) = g0 (σi+1 ) und (Φ(g0 ))(σn ) = id(σn ) = σn . Da g0 aber ein Fixpunkt ist, folgt weiter für i ∈ {0, . . . , n − 1}: g0 (σi ) = g0 (σi+1 ) und g0 (σn ) = σn und damit insbesondere g0 (σ0 ) = σn . Folglich stimmen alle Fixpunkte auf Argumenten s 0 dieser ersten Kategorie überein. Im zweiten Fall erhalten wir ähnlich: (Φ(g0 ))(σi ) = g0 (σi+1 ) für i ∈ {0, . . . , n − 1} und (Φ(g0 ))(σn ) = undefiniert. Damit folgt aus der Fixpunkteigenschaft von g 0 , daß g0 (σ0 ) undefiniert ist. Insbesondere gleichen sich alle Fixpunkte auch auf Argumenten der zweiten Kategorie. Mögliche Unterschiede zwischen Fixpunkten ergeben sich nur in der dritten Kategorie. Hier erhalten wir g0 (σ0 ) = g0 (σi ) für alle i ∈ N. Das legt den Wert g0 (σ0 ) aber nicht fest. Dieses Phänomen ist nicht überraschend, da das zu while (true) {skip}“ gehörige Funk” tional Φ offenbar die identische Funktion ist, die jedes Element auf sich selbst abbildet. Auf der anderen Seite verlangt das intuitive (bzw. operationelle) Verständnis, daß [[ while (true) {skip} ]]ds undefiniert ist. Auf die allgemeine Situation übertragen bedeutet das: 64 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK fix Φ ist der am wenigsten definierte Fixpunkt von Φ. Für unsere theoretischen Betrachtungen im Rahmen der Fixpunkttheorie muß diese Charakterisierung umformuliert werden. Dazu definieren wir zwischen partiellen (d.h. nicht überall definierten) Funktionen g1 , g2 : Σ− →Σ eine Ordnungsbeziehung, g1 g2 , falls • g2 überall dort definiert ist, wo g1 definiert ist, und sich • g1 und g2 auf dem gemeinsamen Definitionsbereich gleichen. Das ist äquivalent dazu, daß der Graph graph(g 1 ) von g1 im Graphen graph(g2 ) von g2 enthalten ist. Formal läßt sich das wie folgt schreiben: g1 g2 genau dann, wenn graph(g1 ) ⊆ graph(g2 ), wobei graph(g) ⊆ Σ × Σ mit (σ, σ 0 ) ∈ graph(g), falls g(σ) = σ 0 . Anschaulich gilt g1 g2 also genau dann, wenn g1 höchstens an den Stellen definiert ist, an denen g2 definiert ist, und die partiellen Funktionen g 1 und g2 an allen Stellen, an denen beide definiert sind, denselben Funktionswert besitzen. Natürlich ist diese Ordnung nicht total, wie das folgende Beispiel zeigt. Beispiel 2.14 Seien g1 , g2 , g3 wie folgt definiert: ( σ undefiniert falls σ(x) gerade ist sonst σ undefiniert falls σ(x) ungerade ist sonst g1 (σ) =df g2 (σ) =df ( g3 (σ) =df σ Dann gilt: g1 g3 und g2 g3 . Aber es gilt weder g1 g2 noch g2 g1 . 3 Mit Hilfe dieser Ordnung auf partiellen Funktionen können wir fix Φ wie folgt charakterisieren: • fix Φ ist ein Fixpunkt von Φ, d.h. fix Φ = Φ(fix Φ). • fix Φ ist der kleinste Fixpunkt von Φ, d.h. ∀g0 . Φ(g0 ) = g0 impliziert fix Φ g0 . Diese Charakterisierung impliziert automatisch die Eindeutigkeit von fix Φ. Der folgende Abschnitt widmet sich nun der noch fehlenden Existenzgarantie. 2.3. SEMANTIK VON WHILE 65 Elementare Fixpunkttheorie Zum Existenznachweis entwickeln wir nun die sogenannte ‘elementare Fixpunkttheorie’. Das erfordert zunächst die Einführung einiger neuer Begriffe: Definition 2.15 (Halbordnung) Sei D eine beliebige Menge. a) Ist vD eine Relation auf D derart, daß für alle d1 , d2 , d3 ∈ D gilt: – d1 vD d1 (Reflexivität), – d1 vD d2 ∧ d2 vD d3 impliziert d1 vD d3 (Transitivität) und – d1 vD d2 ∧ d2 vD d1 impliziert d1 = d2 (Antisymmetrie), so heißt vD Halbordnungsrelation auf D. b) Ein Paar hD; vD i bestehend aus einer Menge D und einer Halbordnungsrelation v D heißt Halbordnung. Im folgenden wird der Index D weggelassen, falls er aus dem Kontext hervorgeht. 3 Beispiel 2.16 hN; ≤i ist eine Halbordnung, aber hN; <i ist keine Halbordnung. 3 Das folgende Lemma etabliert den Bezug der neuen Definition zu WHILE. Hier bezeichne Σ− →Σ die Menge der partiellen Funktionen von Σ in sich, also die Menge aller Zustandstransformationen. Der Beweis des Lemmas ist eine leichte Übungsaufgabe. Lemma 2.17 hΣ− →Σ; i ist eine Halbordnung. 3 Definition 2.18 (Kette, Obere Schranke,Supremum) Sei hD; vD i eine Halbordnung und T ⊆ D. 1. T heißt Kette in D, falls für alle d1 , d2 ∈ T gilt: d1 vD d2 oder d2 vD d1 . 2. Ein Element d ∈ D heißt obere Schranke von T , falls für alle d0 ∈ T gilt: d0 vD d. 3. Eine obere Schranke d von T heißt kleinste obere Schranke (Supremum) von T (in D), falls d kleiner (vD ) als alle anderen oberen Schranken von T ist. 3 66 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Beispiel 2.19 • Jede Teilmenge T ⊆ N ist eine Kette in hN; ≤i. • Jede endliche Teilmenge T ⊆ N besitzt eine (kleinste) obere Schranke in hN; ≤i. 3 Sei T eine Kette in einer Halbordnung hD; vi. T besitze eine kleinste obere Schranke. F Dann bezeichnen wir diese kleinste obere Schranke mit D T . Falls aus dem Kontext unmißverständlich ersichtlich, wird der Index D weggelassen. Definition 2.20 (Kettenvollständigkeit) Eine Halbordnung heißt kettenvollst ändig, falls jede ihrer Ketten eine kleinste obere Schranke besitzt. Solche Halbordnung bezeichnen wir als CCPOs (= Chain Complete Partial Orders). 3 Beispiel 2.21 hP(N); ⊆i ist eine CCPO. hN; ≤i ist keine CCPO, da N keine kleinste obere Schranke in N besitzt. 3 Lemma 2.22 hΣ− →Σ; i ist eine CCPO. Die kleinste obere Schranke zu einer Kette T in dieser CCPO ist gegeben durch: G [ graph( T ) = {graph(g) | g ∈ T }. 3 Beweis F S Nach Lemma 2.17 muß nur noch die Gleichung graph( T ) = {graph(g) | g ∈ T } etabliert werden. Dazu beweisen wir zunächst, daß g0 : Σ− →Σ ist, d.h. (σ, σ 0 ), (σ, σ 00 ) ∈ Seien hierzu (σ, σ 0 ), (σ, σ 00 ) ∈ und g2 in T mit S S [ {graph(g) | g ∈ T } der Graph einer partiellen Funktion {graph(g) | g ∈ T } impliziert σ 0 = σ 00 . {graph(g) | g ∈ T }. Dann gibt es partielle Funktionen g 1 • (σ, σ 0 ) ∈ graph(g1 ) und • (σ, σ 00 ) ∈ graph(g2 ). Da T aber eine Kette ist, gilt entweder g 1 g2 oder g2 g1 . Das liefert aber in beiden Fällen wie gewünscht σ 0 = g1 (σ) = g2 (σ) = σ 00 . 2.3. SEMANTIK VON WHILE 67 S Sei also g0 die durch {graph(g) | g ∈ T } definierte partielle Funktion, d.h. graph(g 0 ) = S {graph(g) | g ∈ T }. Dann ist g0 offensichtlich eine obere Schranke von T , da graph(g 0 ) nach Definition sämtliche Graphen graph(g) zu Elementen g von T enthält. Es bleibt also nur noch zu zeigen, daß g 0 kleinst“ ist. Sei also g1 eine beliebige obere ” Schranke von T . Dann gilt aber graph(g) ⊆ graph(g 1 ) für alle g ∈ T und damit graph(g0 ) = [ {graph(g) | g ∈ T } ⊆ graph(g1 ). 2 Existenz von fix Φ In diesem Abschnitt zeigen wir die Existenz von fix Φ in CCPOs. Basierend auf Lemma 2.22 wird das die Wohldefiniertheit von [[ · ]] ds implizieren. Definition 2.23 (Monotonie) Seien hD; vi und hD 0 ; v0 i zwei CCPOs und f : D −→ D 0 eine (totale) Funktion. Dann heißt f monoton, falls für alle d1 , d2 ∈ D gilt: d1 v d2 impliziert f (d1 ) v0 f (d2 ). 3 Insbesondere ist also jede konstante Funktion und die Identität monoton. Lemma 2.24 Seien hD; vi und hD 0 ; v0 i CCPOs, f : D −→ D 0 eine beliebige Funktion und T ⊆ D eine Kette in D. Dann gilt: 1. Ist f monoton, so ist {f (d) | d ∈ T } eine Kette in D 0 . 2. Ist f monoton, so ist F {f (d) | d ∈ T } v0 f ( F T ). 3 Beweis Zum Beweis der ersten Aussage seien d 01 , d02 ∈ {f (d) | d ∈ T }. Dann gibt es d1 , d2 ∈ T mit f (d1 ) = d01 und f (d2 ) = d02 . Da T eine Kette in D ist, können wir o.B.d.A. davon ausgehen, daß d1 v d2 gilt. Das liefert aber wegen der Monotonie von f d01 = f (d1 ) v0 f (d2 ) = d02 68 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK F Zum Beweis der zweiten Aussage sei d ∈ T . Wegen d v T und der Monotonie von f F F 0 liefert das unmittelbar f (d) v f ( T ). Also ist f ( T ) obere Schranke von {f (d) | d ∈ T } F und damit f (d) v0 f ( T ). 2 Bemerkung 2.25 F Ist hD; vi eine CCPO, so existiert insbesondere ⊥ = df ∅. Weiterhin ist ⊥ das kleinste Element in D, d.h. ∀ d ∈ D. ⊥ v d. Dieses folgt unmittelbar daraus, daß die leere Menge ∅ offensichtlich eine Kette ist und jedes Element von D eine obere Schranke von ∅ ist. Daher ist das Supremum von ∅ gleichzeitig das kleinste Element von D. 3 Definition 2.26 (Stetigkeit) Seien hD; vi und hD 0 ; v0 i zwei CCPOs und f : D −→ D 0 . f heißt stetig, falls f monoton ist und für jede nicht leere Kette T in hD; vi gilt: G G f( T ) = {f (d) | d ∈ T }. 3 Lemma 2.27 Mit f und f 0 ist auch f 0 ◦ f stetig. 3 Beweis Seien hD; vi, hD 0 ; v0 i und hD 00 ; v00 i Halbordnungen, sowie f : D −→ D 0 und f 0 : D 0 −→ D 00 stetige Funktionen. Offenbar ist f 0 ◦ f monoton, denn mit der Monotonie von f und f 0 gilt für d1 , d2 ∈ D: d1 v d2 ⇒ f (d1 ) v0 f (d2 ) ⇒ (f 0 ◦ f )(d1 ) v00 (f 0 ◦ f )(d2 ). Sei weiter T eine nichtleere Kette in D. Da f stetig ist, ist f insbesondere monoton. Also ist {f (d) | d ∈ T } nach Lemma 2.24 eine Kette in D 0 . Also folgt aus der Stetigkeit von f : G G f( T ) = {f (d) | d ∈ T }(∗) Da aber auch f 0 stetig ist, folgt analog {f 0 (d0 ) | d0 ∈ {f (d) | d ∈ T }} = {f 0 (f (d)) | d ∈ T } ist eine Kette in D 00 , und weiter: F F 0 0 f 0 ( {f (d) | d ∈ T } = {f (d ) | d0 ∈ {f (d) | d ∈ T }} = Nach (∗) gilt somit f 0 (f ( G T )) = F {f 0 (f (d)) | d ∈ T } G {f 0 (f (d)) | d ∈ T } 2.3. SEMANTIK VON WHILE 69 und damit wie gewünscht (f 0 ◦ f )( G T) = G {(f 0 ◦ f )(d) | d ∈ T }, 2 Damit läßt sich nun der Hauptsatz dieses Abschnitts beweisen: Satz 2.28 (Fixpunktsatz von Knaster/Tarski bzw. Kleene) Sei hD; vi eine kettenvollständige Halbordnung und f : D −→ D eine stetige Funktion. Dann gilt: G {f n (⊥) | n ∈ N} ist der kleinste Fixpunkt von f in D. Wir bezeichnen ihn mit fixf . 3 Beweis Der Beweis etabliert nacheinander die folgenden drei Eigenschaften von fixf : 1. die Wohldefiniertheit 2. die Fixpunkteigenschaft und 3. die Kleinstheit“. ” F Offenbar is fixf = {f n (⊥) | n ∈ N} wohldefiniert, falls {f n (⊥) | n ∈ N} eine nichtleere Kette in hD; vi ist. Seien also d, d0 ∈ {f n (⊥) | n ∈ N}. Dann müssen wir zeigen, daß d und d0 in unserer Ordung vergleichbar sind. Zunächst liefert uns die Wahl von d und d 0 natürliche Zahlen k und l mit: d = f k (⊥) und d0 = f l (⊥) Sein nun o.B.d.A. k ≤ l. Dann gilt d0 = f m+k (⊥) für m = l − k. Andererseits ist ⊥ das kleinste Element von D. Also gilt insbesondere ⊥ v f m (⊥). Außerdem folgt aus Lemma 2.27 durch eine triviale Induktion, daß mit f auch f k stetig, und damit insbesondere monoton ist. Also gilt wie gewünscht: d = f k (⊥) v f k (f m (⊥)) = f m+k (⊥)) = d0 70 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Die Fixpunkteigenschaft von fixf ist Konsequenz folgender Gleichungskette: f (fixf ) (Definition von fixf ) (Stetigkeit von f ) F = f ( {f n (⊥) | n ∈ N}) = = ( ∅ v d für alle d ∈ D ) = F (Definition von fixf ) F F F {f (f n (⊥)) | n ∈ N} {f n (⊥) | n ∈ N\{0}} {f n (⊥) | n ∈ N} = fixf Um zu zeigen, daß fixf der kleinste Fixpunkt von f ist, betrachten wir einen beliebigen Fixpunkt d ∈ D von f . Da ⊥ das kleinste Element von D ist, folgt für diesen aus der Monotonie von f ∀n ∈ N. f n (⊥) v f n (d) = d Also ist d eine obere Schranke von {f n (⊥) | n ∈ N}, womit wir wie gewünscht fixf = F n {f (⊥) | n ∈ N} v d erhalten. 2 Anwendung auf WHILE Es ist unser nächstes Ziel, zu beweisen, daß [[ while (b) {S} ]]ds = fix Φb, S mit Φb, S (g) = cond([[ b ]]B , g ◦ [[ S ]]ds , id) eine wohldefinierte Funktion ist. Nach Lemma 2.22 ist hΣ− →Σ; i eine CCPO. Also muß nur noch gezeigt werden, daß Φ b, S stetig ist. Dazu zerlegen wir Φb, S in die Funktionen Φb1 (g) = cond([[ b ]]B , g, id) und ΦS2 (g) = g◦[[ S ]]ds . Tatsächlich gilt: Φb, S (g) = Φb1 (ΦS2 (g)) Wegen Lemma 2.27 genügt es also zu zeigen, daß Φb1 und ΦS2 stetig sind. Für Φb1 ist das eine Konsequenz aus folgendem generelleren Resultat: Lemma 2.29 p,g0 0 Sei g0 : Σ− →Σ, p : Σ −→ B und Φp,g stetig. 1 (g) = cond(p, g, g0 ). Dann ist Φ1 Beweis 0 Die Monotonie von Φp,g 1 , d.h. p,g0 0 ∀ g1 , g2 : Σ− →Σ. g1 g2 impliziert (Φp,g 1 (g1 )) (Φ1 (g2 )) 3 2.3. SEMANTIK VON WHILE 71 folgt aus folgender einfachen Fallunterscheidung: p,g0 0 1. p(σ) = tt: Dann ist (Φp,g 1 (g1 ))(σ) = g1 (σ) g2 (σ) = (Φ1 (g2 ))(σ). p,g0 0 2. p(σ) = ff : Hier ist (Φp,g 1 (g1 ))(σ) = g0 (σ) = (Φ1 (g2 ))(σ). Damit reduziert Lemma 2.24 das Problem darauf, G G p,g 0 Φp,g ( T ) {Φ1 0 (g) | g ∈ T } 1 oder gleichbedeutend (Lemma 2.22) G G p,g 0 graph(Φp,g ( T )) ⊆ graph( {Φ1 0 (g) | g ∈ T }) 1 F 0 für jede nichtleere Kette T in Σ− →Σ zu zeigen. Sei also (σ, σ 0 ) ∈ graph(Φp,g T )) 1 ( F 0 0 . Dann genügt es offenbar, die Existenz eines oder gleichbedeutend (Φp,g ( T ))(σ) = σ 1 0 0 Elements g ∈ T mit (Φp,g 1 (g))(σ) = σ nachzuweisen. Im Falle p(σ) = ff ist dies trivial, 0 0 0 da dort (Φp,g 1 (g))(σ) = g0 (σ) = σ für alle g ∈ T gilt. Sei also p(σ) = tt. Dann folgt σ = F F F 0 T ))(σ) = ( T )(σ) und damit (σ, σ 0 ) ∈ graph( T ). Da aber nach Lemma 2.22 (Φp,g 1 ( F S graph( T ) = {graph(g) | g ∈ T } gilt, muß ein Element g ∈ T existieren mit g(σ) = σ 0 , 0 0 2 also mit (Φp,g 1 (g))(σ) = σ . Die Stetigkeit von ΦS2 ist Konsequenz des folgenden Resultats: Lemma 2.30 Sei g0 : Σ− →Σ eine beliebige Zustandstransformation und Φ g20 (g) = g ◦ g0 . Dann ist Φg20 stetig. 3 Beweis Zum Beweis dieses Lemmas benötigen wir das Relationenprodukt ◦ R , daß für zwei binäre Relationen R1 und R2 wie folgt definiert ist: R1 ◦R R2 =df {(x, y) | ∃ z. (x, z) ∈ R1 ∧ (z, y) ∈ R2 }. Für den Nachweis der Monotonie von Φg20 seien g1 , g2 ∈ Σ− →Σ mit g1 g2 . Dann gilt: graph (Φg20 (g1 )) (Definition von Φg20 ) = graph (g1 ◦ g0 ) (Definition von ◦R ) = graph (g0 ) ◦R graph(g1 ) (g1 g2 , Definition von ◦R ) ⊆ graph (g0 ) ◦R graph(g2 ) (Definition von ◦R ) = graph (g2 ◦ g0 ) (Definition von Φg20 ) = graph (Φg20 (g2 )) 72 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Also haben wir Φg20 (g1 ) Φg20 (g2 ). Ähnlich folgt die Stetigkeit von Φg20 aus folgender, für jede nichtleere Kette T in Σ− →Σ gültigen Gleichungskette: graph (Φg20 ( (Definition von Φg20 ) = graph (( F F T )) T ) ◦ g0 ) (Definition von ◦R ) = graph (g0 ) ◦R graph( (Lemma 2.22) = graph (g0 ) ◦R = = (Definition von Φg20 ) = (Lemma 2.22) S S S S F T) {graph(g) | g ∈ T } {graph(g0 ) ◦R graph(g) | g ∈ T } {graph(g ◦ g0 ) | g ∈ T } {graph(Φg20 (g)) | g ∈ T } = graph ( F {Φg20 (g) | g ∈ T }) 2 Zusammenfassend erhalten wir als Konsequenz aus den Lemmata 2.29 und 2.30: Korollar 2.31 Φb, S ist stetig. 3 Damit ist die Hauptarbeit für das folgende Resultat geleistet: Theorem 2.32 (Wohldefiniertheit) Das semantische Funktional [[ · ]]ds : Stm −→ (Σ− →Σ) zur denotationellen Semantik ist wohldefiniert. 3 Beweis Es genügt zu zeigen, das die semantischen Funktionen [[ S ]] ds : Σ− →Σ für alle WHILE– Programme S wohldefiniert sind. Der folgende Beweis ist induktiv über den syntaktischen Aufbau des betrachteten Programms S: • [x = a]: trivial. • [skip]: trivial. • [S1 ; S2 ]: Nach Induktionsvoraussetzung sind [[ S 1 ]]ds und [[ S2 ]]ds wohldefiniert. Also ist auch die Komposition wohldefiniert. 2.3. SEMANTIK VON WHILE 73 • [if (b) {S1 } else {S2 }]: Nach Definition ist [[ if (b) {S1 } else {S2 } ]]ds = cond([[ b ]]B , [[ S1 ]]ds , [[ S2 ]]ds ). Die Behauptung folgt nun aus der Wohldefiniertheit von cond und der Induktionsvoraussetzung für S1 und S2 . • [while (b) {S}]: Nach Induktionsvoraussetzung ist [[ S ]] ds eine wohldefinierte Funktion. Außerdem ist Φb, S nach Korollar 2.31 stetig. Damit können wir den Fixpunktsatz 2.28 anwenden und erhalten, daß fix Φ b, S existiert und deshalb auch [[ while (b) {S} ]]ds wohldefiniert ist. 2 Beispiel 2.33 Wir betrachten das folgende Programm zur ganzzahligen Division mit Rest: S0 S =df z }| { z = 0; while (y ≤ x) {z = z + 1; x = x − y} | {z } SW Dann gilt nach Definition des denotationellen Semantikfunktionals: [[ S ]]ds = [[ SW ]]ds ◦ [[ z = 0 ]]ds oder gleichbedeutend [[ S ]]ds (σ) = [[ SW ]]ds ◦ [[ z = 0 ]]ds (σ) = [[ SW ]]ds (σ{0/z}) Die Semantik der While–Schleife erfordert die Berechnung eines kleinsten Fixpunkts. Diesen berechnen wir mit Hilfe der im Fixpunktsatz von Knaster/Tarski implizit angegebenen Iterationsvorschrift. Zunächst ist dazu das Funktional Φb, S zu bestimmen. Hier gilt: [[ SW ]]ds = fixΦb, S mit ( g ◦ [[ S 0 ]]ds (σ) , falls σ(y) ≤ σ(x) b, S (Φ (g))(σ) = σ , falls σ(y) > σ(x) (sonst) ( g(σ{σ(z) + 1/z}{σ{σ(z) + 1/z}(x) − σ{σ(z) + 1/z}(y)/x}) , falls σ(y) ≤ σ(x) (?) = σ , falls σ(y) > σ(x) + 1/z}{σ(x) − σ(y)/x} ) , falls σ(y) ≤ σ(x) g(σ{σ(z) | {z } = (?) σ1 σ , falls σ(y) > σ(x) 74 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Berechne nun einige Iterationen gemäß dem Fixpunktsatz: 0 n = 0 : (Φb, S (⊥))(σ) = ⊥(σ) = undefiniert(⊥) 1 n = 1 : (Φb, S (⊥))(σ) eingesetzt in (?) mit g = ⊥ 2 = ( ⊥(σ1 ) = ⊥ , falls σ(y) ≤ σ(x) (??) σ , falls σ(y) > σ(x) 1 n = 2 : (Φb, S (⊥))(σ) = Φb, S (Φb, S (⊥))(σ) ( 1 Φb, S (⊥)(σ1 ) , falls σ(y) ≤ σ(x) eingesetzt in (?) = σ , falls σ(y) > σ(x) mit g = Φ(⊥) ) ( ⊥(σ2 ) = ⊥ , falls σ1 (y) ≤ σ1 (x) , falls σ(y) ≤ σ(x) = (??) eingesetzt σ1 , falls σ1 (y) > σ1 (x) für Φ1 (⊥)(σ1 ). σ , falls σ(y) > σ(x) Dabei: σ in (??) ist jetzt σ1 mit σ2 = σ1 {σ1 (z) + 1/z}{σ1 (x) − σ1 (y)/x} Es ist σ1 (y) = σ(y) und σ1 (x) = σ(x) − σ(y). Daher gilt: ⊥ , falls σ(y) ≤ σ(x) − σ(y) und falls σ(y) ≤ σ(x) = σ1 , falls σ(y) > σ(x) − σ(y) und falls σ(y) ≤ σ(x) σ , falls σ(y) > σ(x) ⊥ , falls σ(x) ≥ 2σ(y) = σ1 , falls σ(y) ≤ σ(x) < 2σ(y) (? ? ?) σ , falls σ(x) < σ(y) 3 2 n = 3 : (Φb, S (⊥))(σ) = Φb, S (Φb, S (⊥))(σ) ( 2 Φb, S (⊥)(σ1 ) , falls σ(y) ≤ σ(x) eingesetzt in (?) = σ , falls σ(y) > σ(x) mit g = Φ2 (⊥) ⊥ , falls σ1 (x) ≥ 2σ1 (y) σ2 , falls σ1 (y) ≤ σ1 (x) < 2σ1 (y) (? ? ?) eingesetzt = σ1 , falls σ1 (x) < σ1 (y) für Φ2 (⊥)(σ1 ). σ Dabei ist σ in (? ? ?) jetzt σ1 , falls σ(y) ≤ σ(x) , falls σ(y) > σ(x) 2.3. SEMANTIK VON WHILE ⊥ , σ , 2 = σ1 , σ , ⊥ , σ , 2 = σ1 , σ , i 75 falls falls falls falls σ(x) − σ(y) ≥ 2σ(y) und falls σ(y) ≤ σ(x) σ(y) ≤ σ(x) − σ(y) < 2σ(y) und falls σ(y) ≤ σ(x) σ(x) − σ(y) < σ(y) und falls σ(y) ≤ σ(x) σ(y) > σ(x) falls falls falls falls σ(x) ≥ 3σ(y) 2σ(y) ≤ σ(x) < 3σ(y) σ(y) ≤ σ(x) < 2σ(y) σ(y) > σ(x) i−1 n = i : (Φb, S (⊥))(σ) = Φb, S (Φb, S (⊥))(σ) , falls σ(x) ≥ iσ(y) ⊥ σi−1 , falls (i − 1)σ(y) ≤ σ(x) < iσ(y) .. . = σ2 , falls 2σ(y) ≤ σ(x) < 3σ(y) σ1 , falls σ(y) ≤ σ(x) < 2σ(y) σ , falls σ(x) < σ(y) Der Fixpunkt fixΦb, S ergibt sich jetzt als folgende unendliche Fallunterscheidung: .. . σi−1 , falls (i − 1)σ(y) ≤ σ(x) < iσ(y) .. b, S (fixΦ )(σ) = . .. . σ , falls σ(x) < σ(y) Beachte: Der ⊥-Fall ist in der obigen Fallunterscheidung nicht mehr vorhanden. mit σk = σk−1 {σk−1 (z) + 1/z}{σk−1 (x) − σk−1 (y)/x} = .. . σ{σ(z) + (i − 1)/z}{σ(x) − (i − 1)σ(y)/x} .. . .. . σ , falls (i − 1)σ(y) ≤ σ(x) < iσ(y) , falls σ(x) < σ(y) Für die denotationelle Semantik von S bzgl. σ erhalten wir folglich: 76 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK [[ S ]]ds (σ) = [[ SW ]]ds (σ{0/z}) = fixΦb, S (σ{0/z}) .. . = σ{k − 1/z}{σ(x) − (k − 1)σ(y)/x} , falls (k − 1)σ(y) ≤ σ(x) < kσ(y) .. . = σ{σ(x) DIV σ(y)/z}{σ(x) MOD σ(y)/x} 3 Beispiel 2.34 Als zweites Beispiel betrachten wir das Fakultätprogramm: S =df y = 1; while (1 < x) {y = y ∗ x; x = x − 1}. Sei zur Abkürzung b =df 1 < x und S 0 =df y = y ∗ x; x = x − 1. Hier gilt für einen beliebigen Startzustand σ0 : [[ S ]]ds (σ0 ) = (fix Φb, S )(σ) für σ = σ0 {1/y} Dann ist Φ =df Φb, S wie folgt definiert: (Φ(g))(σ) =df ( g(σ{σ(y) ∗ σ(x)/y}{σ(x) − 1/x}) σ falls σ(x) > 1 sonst Wir können nun die Approximationen für (fix Φb, S )(σ) bestimmen: • Φ0 (⊥)(σ) = undefiniert ( undefiniert 1 • Φ (⊥)(σ) = σ falls σ(x) > 1 sonst undefiniert 2 • Φ (⊥)(σ) = σ{σ(x) ∗ σ(y)/y}{σ(x) − 1/x} σ falls σ(x) > 2 falls σ(x) = 2 sonst undefiniert n • Φ (⊥)(σ) = σ{σ(y) ∗ σ(x) ∗ (σ(x) − 1) ∗ ... ∗ 1/y}{1/x} σ Das bedeutet für den Fixpunkt: ( σ{σ(y) ∗ σ(x) ∗ (σ(x) − 1) ∗ ... ∗ 1/y}{1/x} (fix Φb, S )(σ) = σ Im Falle σ0 (x) = 3 gilt also (fix Φ)(σ0 ) = 1 ∗ 3 ∗ 2 ∗ 1 = 6. falls σ(x) > n falls σ(x) ∈ {2, ..., n} sonst falls σ(x) > 2 sonst 3 2.3. SEMANTIK VON WHILE 77 Beispiel 2.35 Als zweites Beispiel betrachten wir das Fakultätprogramm: S =df y = 1; while (1 < x) {y = y ∗ x; x = x − 1}. Sei zur Abkürzung b =df 1 < x und S 0 =df y = y ∗ x; x = x − 1. Hier gilt für einen beliebigen Startzustand σ0 : [[ S ]]ds (σ0 ) = (fix Φb, S )(σ) für σ = σ0 {1/y} Dann ist Φ =df Φb, S wie folgt definiert: ( g(σ{(σ(y) ∗ σ(x))/y}{(σ(x) − 1)/x}) (Φ(g))(σ) =df σ falls σ(x) > 1 sonst Wir können nun die Approximationen für (fix Φb, S )(σ) bestimmen: • Φ0 (⊥)(σ) = undefiniert ( undefiniert falls σ(x) > 1 1 • Φ (⊥)(σ) = σ sonst falls σ(x) > 2 undefiniert 2 • Φ (⊥)(σ) = σ{(σ(x) ∗ σ(y))/y}{(σ(x) − 1)/x} falls σ(x) = 2 σ sonst falls σ(x) > n undefiniert n • Φ (⊥)(σ) = σ{(σ(y) ∗ σ(x) ∗ (σ(x) − 1) ∗ ... ∗ 1)/y}{1/x} falls σ(x) ∈ {2, ..., n} σ sonst Das bedeutet für den Fixpunkt: ( σ{(σ(y) ∗ σ(x) ∗ (σ(x) − 1) ∗ ... ∗ 1)/y}{1/x} (fix Φb, S )(σ) = σ Im Falle σ0 (x) = 3 gilt also (fix Φ)(σ0 ) = 1 ∗ 3 ∗ 2 ∗ 1 = 6. falls σ(x) > 2 sonst 3 Das Koinzidenztheorem Das folgende Theorem bestätigt die Konsistenz unserer bisherigen Semantikbetrachtungen: Die operationelle und die denotationelle Semantik stimmen überein. Theorem 2.36 (Koinzidenztheorem) ∀ S ∈ Stm. [[ S ]]ds = [[ S ]]sos 3 78 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Beweis Wegen der Antisymmetrie von “ folgt die Behauptung aus den beiden folgenden Lem” mata. 2 Lemma 2.37 ∀ S ∈ Stm. [[ S ]]sos [[ S ]]ds . 3 Beweis Offenbar genügt es, für alle Zustände σ, σ 0 die Aussage (∗) hS, σi ⇒∗ σ 0 impliziert [[ S ]]ds (σ) = σ 0 zu zeigen. Ein einfaches Induktionsargument (nach der Länge der Berechnungsfolge) reduziert (∗) auf • hS, σi ⇒ σ 0 impliziert [[ S ]]ds (σ) = σ 0 und • hS, σi ⇒ hS 0 , σ 0 i impliziert [[ S ]]ds (σ) = [[ S 0 ]]ds (σ 0 ). Wir beweisen die reduzierte Variante durch Induktion nach der Struktur des Ableitungsbaums für den betrachteten Transitionsschritt. • Axiome: Für die Axiome x = a“ und skip“ ist die Sache trivial. ” ” • [comp1sos ]: Hier gilt o.B.d.A.hS1 ; S2 , σi ⇒ hS10 ; S2 , σ 0 i wegen hS1 , σi ⇒ hS10 , σ 0 i. Damit kann die Induktionsvoraussetzung auf hS1 , σi ⇒ hS10 , σ 0 i angewendet werden und man erhält [[ S1 ]]ds (σ) = [[ S10 ]]ds (σ 0 ). Wegen der Kompositionalität von [[ · ]]ds folgt damit wie gewünscht: [[ S2 ]]ds ([[ S1 ]]ds (σ)) = [[ S2 ]]ds ([[ S10 ]]ds (σ 0 )) = [[ S10 ; S2 ]]ds (σ 0 ). • [comp2sos ]: Hier gilt hS1 ; S2 , σi ⇒ hS2 , σ 0 i wegen hS1 , σi ⇒ σ 0 . Damit kann die Induktionsvoraussetzung auf hS1 , σi ⇒ σ 0 angewendet werden und man erhält: [[ S1 ]]ds (σ) = σ 0 . Wegen der Kompositionalität von [[ · ]]ds folgt damit: [[ S1 ; S2 ]]ds (σ) = [[ S2 ]]ds ([[ S1 ]]ds (σ)) = [[ S2 ]]ds (σ 0 ). • [iftt sos ]: 2.3. SEMANTIK VON WHILE 79 Hier gilt [[ b ]]B (σ) = tt und hif (b) {S1 } else {S2 }, σi ⇒ hS1 , σi, und die Behauptung folgt aus: [[ if (b) {S1 } else {S2 } ]]ds (σ) (Definition von [[ · ]]ds ) = cond([[ b ]]B (σ), [[ S1 ]]ds (σ), [[ S2 ]]ds (σ)) ([[ b ]]B (σ) = tt) = [[ S1 ]]ds (σ) • [ifff sos ]: Hier gilt [[ b ]]B (σ) = ff und hif (b) {S1 } else {S2 }, σi ⇒ hS2 , σi, und die Behauptung folgt aus: [[ if (b) {S1 } else {S2 } ]]ds (σ) (Definition von [[ · ]]ds ) = cond([[ b ]]B (σ), [[ S1 ]]ds (σ), [[ S2 ]]ds (σ)) ([[ b ]]B (σ) = ff ) = [[ S2 ]]ds (σ) • [whilesos ]: Hier gilt hwhile (b) {S 0 }, σi ⇒ hif (b) {S 0 ; while (b) {S 0 }} else {skip}, σi. Also gilt nach Definition von [[ · ]]ds : [[ while (b) {S 0 } ]]ds = fix Φb, S 0 0 0 (fix Φb, S ist Fixpunkt) = Φb, S (fix Φb, S ) 0 0 (Definition von Φb, S ) = cond([[ b ]]B , fix Φb, S ◦ [[ S 0 ]]ds , id) (Definition von [[ · ]]ds ) = cond([[ b ]]B , [[ S ]]ds ◦ [[ S 0 ]]ds , id) (Definition von [[ · ]]ds = cond([[ b ]]B , [[ S 0 ; while (b) {S 0 } ]]ds , id) (Definition von [[ · ]]ds ) = [[ if (b) {S 0 ; while (b) {S 0 }} else {skip} ]]ds 0 Per Induktionsprinzip folgt die Behauptung. 2 Lemma 2.38 ∀ S ∈ Stm. [[ S ]]ds [[ S ]]sos . 3 80 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Das Lemma kann durch strukturelle Induktion über den syntaktischen Aufbau von S bewiesen werden. Im Induktionsschluss ist im Fall S = while (b) {S} das Lemma von Park hilfreich. Dieses besagt, dass der kleinste Fixpunkt einer stetigen Funktion f auf einer CCPO hD; vi auch der kleinste Präfixpunkt von f ist. Dabei heißt ein Punkt d ∈ D Präfixpunkt von f , wenn f (d)vd ist (fixf ist natürlich - wie übrigens jeder andere Fixpunkt von f auch - ein Präfixpunkt von f ). Lemma 2.39 (Park’s Lemma) Gegeben sei eine CCPO hD; vi, eine stetige Abbildung f : D → D sowie ein Punkt d ∈ D. Falls f (d) v d, so ist fixf v d. 3 Die Durchführung der Beweise von Lemma 2.38 und 2.39 bleibt dem Leser als Übungsaufgabe überlassen. 2.3. SEMANTIK VON WHILE 2.3.4 81 Axiomatische Semantik In diesem Abschnitt wollen wir die Semantik eines WHILE–Programms axiomatisch definieren. Grundlegend dafür sind Aussagen der Form {P } S {Q}, die als Hoare–Tripel (syntaktische Sicht) oder als Korrektheitsformeln (semantische Sicht) bezeichnet werden. Dabei sind {P } und {Q} logische Spezifikationen, die Aussagen über das Programm vor Ausführung von S (Vorbedingung P ) bzw. nach Ausführung von S (Vorbedingung Q) machen, und S eine Anweisung der Sprache WHILE. Im folgenden werden wir zunächst die Syntax und Semantik der Formelsprache beschreiben, die in den Spezifikationen Verwendung findet. Boolesche Formeln zur Programmspezifikation Boolesche Formeln (BF) zur Programmspezifikation basieren im wesentlichen auf den Booleschen Ausdrücken Bexp aus der Sprachbeschreibung von WHILE–Programmen in Kapitel 2.2. Es gibt jedoch zwei wesentliche Unterschiede: • Boolesche Formeln besitzen zusätzlich All- und Existenzquantoren, die es ermöglichen über ganzzahlige Variablen zu quantifizieren. • Wir verwenden statt der an Java angelehnten Booleschen Operatoren und arithmetischen Relationssymbole entsprechende Operatoren, wie sie aus der Mathematik bekannt sind. So verwenden wir beispielsweise ∨ statt && und = statt == . Die Erweiterung um Quantoren erhöht die Ausdruckskraft Boolescher Ausdrücke beträchtlich, ist aber notwendig um Programmeigenschaften von WHILE–Programmen genau genug erfassen zu können. Wir werden später Beispiele kennenlernen, in denen ausgiebig von quantifizierten Formeln Gebrauch gemacht wird. Die Konversion Boolescher Ausdrücke von Bexp nach BF ist offensichtlich. Aus diesem Grund werden wir Bexp-Ausdrücke implizit mit BF-Formeln identifizieren. Dadurch können insbesondere die Bedingungen von WHILE-Programmen auch direkt als Teil von Programmspezifikationen verwendet werden. Die BNF für Boolesche Formeln BF ist im folgenden angegeben, wobei a als Metavariable für arithmetische Ausdrücke aus Kapitel 2.2, b als Metavariable für Boolesche Formeln und x als Metavariable für ganzzahlige Variablen dient: b ::= true | f alse | (a = a) | (a < a) | (a ≤ a) | (¬b) | (b ∧ b) | (b ∨ b) | (∀ x. b) | (∃ x. b) 82 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Es sei darauf hingewiesen, daß Boolesche Formeln somit sowohl Variablen enthalten können, die in einem zugrunde liegenden Programm S vorkommen, als auch solche die nicht in S vorkommen. Derartige Variablen müssen auch nicht an einen Quantor gebunden sein. Oft werden sie benutzt um in der Vorbedingung “Startwerte” der Programmvariablen festzuhalten, um diese dann in der Nachbedingung zu verwenden. Die Semantik Boolescher Formeln und die Definitionen für “freie Variablen” kann im wesentlichen auf die induktiven Definitionen für Boolesche Ausdrücke aus Bexp zurückgeführt werden. Wir verzichten hier auf eine Wiederholung der Definitionen, die sich nur in den Operatorsymbolen unterscheiden würde. Für quantifizierte Formeln werden die induktiven Definitionen allerdings wie folgt ergänzt. • Die Semantikfunktion [[ · ]]B : BF → (Σ → B) wird induktiv erweitert durch: [[ ∀ x. b ]]B (σ) =df tt gdw. [[ b ]]B (σ{z/x}) = tt für alle z ∈ Z [[ ∃ x. b ]]B (σ) =df tt gdw. [[ b ]]B (σ{z/x}) = tt für ein z ∈ Z • Die Menge der freien Variablen FV : Aexp ∪ BF → P(Var) wird induktiv erweitert durch: FV(∀ x. b) =df FV(b) \ {x} FV(∃ x. b) =df FV(b) \ {x} Das heißt, in einer quantifizierten Formel (∀ x. b) oder (∀ x. b) werden alle freien Vorkommen von x in b an den Quantor gebunden und sind damit nicht mehr frei. Auch die syntaktische Substitution wird auf Boolesche Formeln erweitert. Dieses ist bis auf die quantifizierten Formeln vollkommen analog zu der induktiven Definition f ür arithmetische Ausdrücke. Im Falle quantifizierter Formeln muss allerdings vor der Substitution im Rumpf der Formel die gebundene Variable ggf. umbenannt werden. Denn es ist zu vermeiden, daß bei der Substitution Vorkommen dieser Variablen, die bislang frei im einzukopierenden Ausdruck sind, fälschlicherweise an den Quantor gebunden werden. Die syntaktische Substitution wie unten spezifiziert ist daher nur bis auf die Umbenennung von gebundenen Variablen eindeutig. Wir verwenden dennoch eine Funktionsnotation ·[·/·] : BF × Aexp × Var → BF, was dadurch zu rechtfertigen ist, daß die Umbenennung fest, aber beliebig vorgenommen werden kann. Es sei außerdem angemerkt, daß in der späteren Anwendung kein Bedarf an Umbennungen entsteht, wenn Variablen hinter Quantoren stets so gewählt werden, daß diese nicht in dem zugrundeliegenden Programm vorkommen. 2.3. SEMANTIK VON WHILE true[a/x] =df true f alse[a/x] =df f alse (a1 = a2 )[a/x] =df (a1 [a/x] = a2 [a/x]) (a1 < a2 )[a/x] =df (a1 [a/x] < a2 [a/x]) (a1 ≤ a2 )[a/x] =df (a1 [a/x] ≤ a2 [a/x]) (¬b)[a/x] =df (¬b[a/x]) (b1 ∧ b2 )[a/x] =df (b1 [a/x] ∧ b2 [a/x]) (b1 ∨ b2 )[a/x] =df (b1 [a/x] ∨ b2 [a/x]) (∀ x. b)[a/y] =df (∀ z. b[z/x][a/y]) wobei z ∈ / FV(a) ∪ FV(b) ∪ {y} (∃ x. b)[a/y] =df (∃ z. b[z/x][a/y]) wobei z ∈ / FV(a) ∪ FV(b) ∪ {y} 83 Wie bislang gewohnt, verzichten wir auch in Booleschen Formeln auf unnötige Klammersymbole und verwenden zusätzliche Abkürzungen. Dabei folgen wir den Konventionen aus 1.32 und 1.33. Die Substitutionsoperation hat wie bisher die höchste Präzedenz, gefolgt von den Relationsoperatoren, gefolgt von der Negation, gefolgt vom Konjunktionsoperator, gefolgt vom Disjunktionsoperator. Der All- und Existenzquantor hat schließlich die niedrigste Präzedenz unter allen Operatoren. Partielle und totale Korrektheit Im folgenden werden wir zwei Varianten der Definition einer Semantik für Korrektheitsformeln {P } S {Q} vorstellen, die zum einen die partielle Korrektheit und zum anderen die totale Korrektheit respektieren. Dabei bezeichnet [[ S ]] = [[ S ]] sos = [[ S ]]ds die operationelle bzw. die denotationelle Semantik von S (vgl. Theorem 2.36). Zunächst benötigen wir jedoch den Begriff der Charakterisierung eines Booleschen Ausdrucks. Definition 2.40 (Charakterisierung) Sei P eine Boolesche Formel. Dann heißt Ch(P ) =df {σ ∈ Σ | [[ P ]]B (σ) = tt} die Charakterisierung von P . 3 84 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK In den folgenden Abschnitten werden wir in Beweisen mehrere Eigenschaften der Charakterisierung einer Booleschen Formel ausnutzen, welche unmittelbar aus der Definition der Charakterisierung folgen. Lemma 2.41 (Eigenschaften von Ch(·)) Seien P, Q Boolesche Formeln. Dann gilt: 1. Ch(P ∧ Q) = Ch(P ) ∩ Ch(Q) 2. Ch(P ∨ Q) = Ch(P ) ∪ Ch(Q) 3. Ch(¬P ) = Σ \ Ch(P ) 3 Bezeichnen wir nun die Menge aller Zustände, für die [[ S ]] definiert ist, mit Def ([[ S ]]), so können wir die Semantik von Korrektheitsformeln wie folgt definieren: Definition 2.42 (Semantik von Korrektheitsformeln) Eine Korrektheitsformel {P } S {Q} heißt • partiell korrekt, falls [[ S ]](Ch(P )) ⊆ Ch(Q). Dabei sei die Semantikfunktion erweitert auf Zustandsmengen Φ ⊆ Σ durch: [[ S ]](Φ) =df {[[ S ]](σ) | σ ∈ (Φ ∩ Def ([[ S ]]))}. • total korrekt, falls {P } S {Q} partiell korrekt ist und Def ([[ S ]]) ⊇ Ch(P ) gilt. 3 Intuitiv ist eine Korrektheitsformel {P } S {Q} partiell korrekt, falls jeder terminierende Programmlauf, der in einem Zustand startet, der P erfüllt, in einem Zustand terminiert, der Q erfüllt. Sie ist total korrekt, falls jeder Programmlauf, der in einem Zustand startet, welcher P erfüllt, terminiert, und zwar in einem Zustand, der Q erfüllt. Der Unterschied zwischen den beiden Erfüllbarkeitskonzepten besteht also darin, daß im Falle der totalen Korrektheit die Korrektheitsformel auch die Termination impliziert, wohingegen im Falle der partiellen Korrektheit über divergierende (nicht terminierende) Programme nichts ausgesagt wird. Wollen wir eine Korrektheitsformel nur bzgl. partieller (totaler) Korrektheit betrachten, so sprechen wir von einer partiellen (totalen) Korrektheitsformel. Wir werden uns im folgenden vorwiegend mit der partiellen Korrektheit beschäftigen. 2.3. SEMANTIK VON WHILE 85 Beispiel 2.43 • {x = x0 } x = x2 {x = x20 } ist partiell und total korrekt. • {true} x = 0 {x ≤ 7} ist partiell und total korrekt. • {false} x = 35 {x = 0} Da false durch die leere Menge charakterisiert ist, ist diese Korrektheitsformel trivialerweise partiell und total korrekt. • {true} S {false} mit einem beliebigen WHILE–Programm S: Diese Korrektheitsformel beschreibt den Fall, daß aus jedem beliebigen Startzustand kein Endzustand erreicht wird. Dies ist nur dann möglich, wenn das Programm S niemals terminiert (immer divergiert), wie z.B. while (true) {skip}. Die Korrektheitsformel ist für ein immer divergierendes Programm partiell aber nicht total korrekt, für ein Programm, das für mindestens eine Eingabe terminiert, aber nicht partiell und nicht total korrekt. 3 Der folgenden Abschnitt stellt eine streng formale Beweismethode für partielle Korrektheitsformeln vor: den Hoare–Kalkül. Dabei bedeutet ‘streng formal’ regelbasiert. Zum Vergleich: Auch die strukturierte operationelle Semantik wurde regelbasiert angegeben. Eine solche streng formale, syntax–basierte Beschreibung eignet sich besonders gut f ür eine maschinelle Realisierung: bei der strukturierten operationellen Semantik zur Implementierung eines Interpreters und bei dem im folgenden vorgestellten Hoare–Kalk ül zur (halb-)automatischen Verifikation von Programmen. Anschließend wird die Korrektheit und die Vollständigkeit des Hoare–Kalküls relativ zur operationellen (bzw. denotationellen) Semantik untersucht. Der Hoare–Kalkül Ähnlich zum Regelsystem zur strukturierten operationellen Semantik besteht der Hoare– Kalkül aus Regeln der Form Name Prämisse . Konklusion Dabei ist die Prämisse im wesentlichen aus Korrektheitsformeln (keine, eine oder zwei) zusammengesetzt, wohingegen die Konklusion aus genau einer Korrektheitsformel besteht. Regeln ohne Prämisse heißen auch hier Axiome. 86 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Wir wollen nun die einzelnen Regeln dieses Regelsystems vorstellen. Der Hauptteil des Regelsystems ist syntaxtorientiert: jedem WHILE-Konstruktor ist (genau) eine Regel zugeordnet. • skip–Regel: SKIP − {P } skip {P } Die Semantik von skip ist die identische Abbildung auf dem Speicherzustand. • Assignment–Regel: ASS − {Q[t/x]} x = t {Q} Die Assignment–Regel ist ebenfalls ein Axiom. Dabei ist die Substitution [t/x] ( t ” für x“) eine rein syntaktische Operation. Q[t/x] geht folglich aus der Formel Q durch die syntaktische Ersetzung von x durch t hervor. • Kompositionsregel: KOM P {P } S1 {P 0 } {P 0 } S2 {Q} {P } S1 ; S2 {Q} Diese Regel beschreibt, wie sich die partielle Korrektheit von Korrektheitsformeln für S1 und S2 auf Korrektheitsformeln für die sequentiellen Komposition S 1 ; S2 weitervererbt. • if-then-else–Regel: IT E {P ∧ b} S1 {Q} {P ∧ ¬b} S2 {Q} {P } if (b) {S1 } else {S2 } {Q} Bei dieser Regel ist beachtenswert, wie sich der Effekt der bedingten Verzweigung durch die beiden Korrektheitsformeln in der Prämisse widerspiegelt. • while–Regel: W HILE {I ∧ b} S {I} {I} while (b) {S} {I ∧ ¬b} Dabei repräsentiert die Formel I eine sog. Schleifeninvariante: Eine Invariante ist eine Formel, deren Gültigkeit in jeder Schleifeniteration erhalten bleibt. Invarianten erlauben es, Aussagen über Schleifen durch Induktion zu beweisen. Die Invariante selbst steht für die zu beweisende Behauptung. Induktionsanfang ist der Beweis der Gültigkeit der Invariante vor Betreten der Schleife, und der Induktionsschritt ist der Beweis der Invarianz unter den Schleifeniterationen. Die Prämisse der while–Regel formuliert die Invarianteneigenschaft: Gilt I und ist b erfüllt, d.h. die Schleife wird (noch) einmal durchlaufen, so erhält der Schleifenrumpf 2.3. SEMANTIK VON WHILE 87 S die Gültigkeit der Formel I. Die Konklusion der while–Regel besagt: Gilt vor Ausführung der Schleife die Invariante I ( Induktionsanfang“), so gilt nach dem Verlassen der Schleife (falls diese ” terminiert) ebenfalls I, und die Abbruchbedingung ¬b ist erfüllt. Beachte: Es gibt zwar immer die trivialen Invarianten true und false, ein sinnvoller Beweis benötigt aber in der Regel andere, komplexere Invarianten. Diese zu ermitteln ist eine der Hauptschwierigkeiten im Umgang mit dem Hoare–Kalkül. Außer diesen rein syntax-orientierten Regel benötigen wir noch eine weitere Regel zur Adaption der Vor– und Nachbedingungen. • Konsequenzregel: KON S P ⇒P0 {P 0 } S {Q0 } {P } S {Q} Q0 ⇒ Q Dabei steht ⇒“ für die logische Implikation. Die Konsequenzregel besagt, daß die ” partielle Korrektheitsformel {P } S {Q} gültig ist, wenn die Vorbedingung P die Formel P 0 impliziert, {P 0 } S {Q0 } partiell korrekt ist und Q0 wiederum die Nachbedingung Q impliziert. Oder intuitiv: Korrektheitsformeln (hier {P 0 } S {Q0 }) dürfen abgeschwächt werden, indem man die Vorbedingung verschärft und die Nachbedingung abschwächt. Bemerkung 2.44 Wegen der Form der Konklusion wird die Assignment-Regel auch R ückwärts–Assignment–Regel genannt: Aus der Nachbedingung Q einer Zuweisung x = t läßt sich mit Hilfe der Assignment–Regel eine gültige Vorbedingung Q[t/x] konstruieren. Tatsächlich gibt es zu dieser Regel auch eine äquivalente Vorwärts–Variante: ASSv − {P } x = t {∃ n. (P [n/x] ∧ t[n/x] = x)} deren Korrektheit sich mit Hilfe der Rückwärts–Assignment–Regel beweisen läßt. Um diese Regel besser begreifbar zu machen, wollen wir kurz erläutern, warum die Vorwärtsvariante nicht einfach wie folgt aussehen kann: − {P } x = t {P [t/x]} Diese Regel würde beispielsweise die partielle Korrektheit der Korrektheitsformel {x = 3} x = 5 {5 = 3} rechtfertigen, was offensichtlich falsch wäre. 3 Die Bedeutung des Hoare–Regelsystems besteht darin, daß es erlaubt, formale Beweise f ür die partielle Korrektheit einer Korrektheitsformel {P } S {Q} zu führen. Das funktioniert wie folgt: 88 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK • Versuche die aktuell betrachtete Korrektheitsformel mit Hilfe der Konsequenzregel so zu modifizieren, daß die dem führenden Programmkonstruktor zugeordnete Regel anwendbar wird. Dieser Schritt kann in Sonderfällen überflüssig sein. • Wende die dem führenden Programmkonstruktor zugeordnete Regel an. Ist das nicht möglich, d.h. kann die Korrektheitsformel nicht mit Hilfe der Konsequenzregel geeignet adaptiert werden, so scheitert der Beweis. Bis auf die drei folgenden Punkte ist dieses Vorgehen schematisch: • Die Auswahl der geeigneten adaptierenden Abschwächungsschritte und der Beweis der Gültigkeit der verwendeten Implikationen. • Das Finden der Invarianten. • Die Auswahl der als Verbindungsglied dienenden Booleschen Formel bei der Anwendung der Kompositionsregel. Während die ersten beiden Punkte die tatsächlichen Schwierigkeiten im Umgang mit dem Hoare–Kalkül charakterisieren, erfordert der dritte Punkt nur eine systematische Beweiskonstruktion: Die Nachbedingung muß sukzessive entgegen dem üblichen Kontrollfluß solange umgewandelt werden, bis das Programm rückwärts voll abgearbeitet ist. Die Notwendigkeit der Konsequenzregel wird anhand der folgenden, offensichtlich partiell korrekten Korrektheitsformel deutlich: {x = 3} x = 5 {x = 5}. Ein formaler Beweis ohne Konsequenzregel ist zum Scheitern verurteilt, da nur die Assignment–Regel mit dem Resultat {5 = 5} x = 5 {x = 5} angewendet werden kann. Danach stagniert der Beweisversuch, da die Kluft zwischen den Vorbedingungen {x = 3} und {5 = 5} ohne Konsequenzregel nicht überbrückt werden kann. Mit Hilfe der Konsequenzregel kann der Beweis aber erfolgreich fortgesetzt werden: Da 5 = 5 logisch äquivalent zu true ist, gilt (x = 3) ⇒ (5 = 5). Also kann die Konsequenzregel wie folgt eingesetzt werden: KON S (x = 3) ⇒ (5 = 5), {5 = 5} x = 5 {x = 5}, {x = 3} x = 5 {x = 5} (x = 5) ⇒ (x = 5) Zusammen mit der bereits etablierten partiellen Korrektheit von {5 = 5} x = 5 {x = 5} vervollständigt das den formalen Beweis. 2.3. SEMANTIK VON WHILE CONS 89 ASS I’’, {I’’} y=y*m {I’} (I β) COMP {(I WHILE CONS ASS {I’} x=x-1 {I} β)} y=y*m {I’}, {(I β)} y=y*m; x=x-1 {I} I, {I} S {(I ( β)) }, (I {pre} S {post} pre ( β)) post Abbildung 2.6: Beweisbaum für die partielle Korrektheit der Exponentiation Beispiel 2.45 Formale “Hoare-Beweise” notiert man am besten in Form eines Beweisbaumes. Folgendes Programm Sexp Sexp =df z = y; while (!(x == 1)) {y = z ∗ y; x = x − 1} . {z } | SW berechnet für positive Argumente x das Ergebnis der Exponentiation y x . Formal erfülltSexp also die folgende Anforderung an die partielle Korrektheit: {x = n ∧ y = m ∧ n > 0} Sexp {y = mn } {z } | {z } | pre post Der zu einem formalen Hoare-Beweis partieller Korrektheit gehörige Beweisbaum ist in Abbildung 2.6 dargestellt. Zentral ist hierbei die Verwendung der Schleifeninvarianten I =df (y ∗ z x−1 = mn ∧ x > 0). Im Beweisbaum werden zur Vereinfachung der Notation folgende folgende Abkürzungen verwendet: b =df ¬(x = 1) Iz =df I[y/z] Ix =df I[x − 1/x] Ixy =df Ix [y ∗ m/y] 90 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Die in den Konsequenzregeln auftretenden Implikationen sind im Deatail begründet wegen: pre = (x = n ∧ y = m ∧ n > 0) ⇒ (y x = mn ∧ x > 0) ⇒ (y ∗ y x−1 = mn ∧ x > 0) = (I ∧ ¬b) = Iz (y ∗ z x−1 = mn ∧ x > 0 ∧ ¬(¬(x = 1))) ⇒ (y ∗ z x−1 = mn ∧ x = 1) ⇒ (y ∗ z 0 = mn ) ⇒ (y = mn ) (I ∧ b) = post = (y ∗ z x−1 = mn ∧ x > 0 ∧ ¬(x = 1)) ⇒ (y ∗ z x−1 = mn ∧ x > 1) ⇒ (y ∗ z ∗ z x−2 = mn ∧ x − 1 > 0) = ((y ∗ z) ∗ z (x−1)−1 = mn ∧ x − 1 > 0) = Ixy 3 Korrektheit der Methode von Hoare In diesem Abschnitt wollen wir die Korrektheit der Methode von Hoare beweisen. Zunächst betrachten wir den Fall der partiellen Korrektheit. Anschließend wird eine Modifikation zur Erfassung der totalen Korrektheit vorgestellt. Satz 2.46 (Korrektheit des Hoare-Kalk üls für partielle Korrektheit) Mit Hilfe des Hoare–Kalküls lassen sich nur partiell korrekte Korrektheitsformeln ableiten. 3 Beweis Wir beweisen den Satz durch Induktion nach der Anzahl n der verwendeten Beweisschritte (im Beweisbaum). 2.3. SEMANTIK VON WHILE 91 Induktionsanfang: (n = 1) Im ersten (und einzigen) Schritt kann nur ein Axiom angewendet worden sein. Die Korrektheit für den Fall n = 1 folgt also aus der Korrektheit der Skip– und der Assignmentregel. Dazu: 1. skip–Regel: Die hier zu verifizierende Korrektheitsformel {P } skip {P } ist offensichtlich partiell korrekt. 2. Assignment–Regel: Hier ist die partielle Korrektheit von {Q[t/x]} x = t {Q} nachzuweisen. Sei dazu σ ∈ Ch(Q[t/x]). Dann genügt es σ{[[ t ]]A (σ)/x} ∈ Ch(Q) zu zeigen (vgl. dies auch mit der operationellen bzw. denotationellen Semantik). Dieser Zusammenhang (es gilt sogar σ ∈ Ch(Q[t/x]) ⇐⇒ σ{[[ t ]] A (σ)/x} ∈ Ch(Q)) wird üblicherweise als Substitutionslemma bezeichnet (vgl. Kapitel 1, Lemma 1.43 und Kapitel 2, Lemma 2.6). Der zugehörige formale Beweis erfolgt wie üblich mittels struktureller Induktion nach t. Zentral ist dabei wieder die strukturelle Definition von [./.]: Q Q[t/x] = t op(Q1 [t/x], ..., Qn [t/x]) falls Q Konstante oder Variable 6= x falls Q = x falls Q = op(Q1 , ..., Qn ) Siehe Folien zur Vorlesung (Teil 1): Seite 195-198 Für den Induktionsschluß können wir nun nach Induktionsvoraussetzung annehmen, daß alle in höchstens n Schritten abgeleiteten Korrektheitsformeln partiell korrekt sind. Induktionsschluß: (n → n + 1) Hier muß gezeigt werden, daß der n + 1-te Beweisschritt die partielle Korrektheit erhält. Der Beweis erfordert eine Fallunterscheidung bzgl. der dabei angewendeten Regel: 1. Kompositionsregel: D.h. in der folgenden Situation KOM P {P } S1 {P 0 } {P 0 } S2 {Q} {P } S1 ; S2 {Q} muß unter der (Induktions–) Voraussetzung • [[ S1 ]](Ch(P )) ⊆ Ch(P 0 ) • [[ S2 ]](Ch(P 0 )) ⊆ Ch(Q). 92 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK gezeigt werden, daß [[ S1 ; S2 ]](Ch(P )) ⊆ Ch(Q) gilt. Dies folgt aber unmittelbar aus [[ S1 ; S2 ]] = [[ S2 ]] ◦ [[ S1 ]]. 2. if-then-else–Regel: D.h. in der Situation IT E {P ∧ b} S1 {Q} {P ∧ ¬b} S2 {Q} {P } if (b) {S1 } else {S2 } {Q} muß unter der (Induktions–) Voraussetzung • [[ S1 ]](Ch(P ∧ b)) ⊆ Ch(Q) • [[ S2 ]](Ch(P ∧ ¬b)) ⊆ Ch(Q) gezeigt werden, daß [[ if (b) {S1 } else {S2 } ]](Ch(P )) ⊆ Ch(Q) gilt. Zum Beweis nutzen wir [[ if (b) {S1 } else {S2 } ]](σ) =df ( [[ S1 ]](σ) [[ S2 ]](σ) falls [[ b ]]B (σ) = tt falls [[ ¬b ]]B (σ) = tt Sei nun σ ∈ Ch(P ) beliebig und o.B.d.A. [[ b ]] B (σ) = tt (der Fall [[ b ]]B (σ) = ff geht analog). Dann gilt σ ∈ Ch(P ∧ b), also nach Induktionsvoraussetzung [[ S 1 ]]({σ}) ⊆ Ch(Q). Andererseits gilt in diesem Falle unmittelbar nach der Definition der Semantik [[ S1 ]](σ) = [[ if (b) {S1 } else {S2 } ]](σ), und damit folgt die Behauptung. 3. While–Regel: D.h. in der Situation W HILE {I ∧ b} S {I} , {I} while (b) {S} {I ∧ ¬b} muß unter der (Induktions–) Voraussetzung [[ S ]](Ch(I ∧ b)) ⊆ Ch(I) gezeigt werden, daß [[ while (b) {S} ]](Ch(I)) ⊆ Ch(I ∧ ¬b) gilt. Punktweise, also für jeden Zustand σ ∈ Ch(I), heißt das: [[ while (b) {S} ]]({σ}) ⊆ Ch(I ∧ ¬b) Im Falle der Divergenz der While–Schleife für σ ist die Behauptung trivial, da [[ while (b) {S} ]]({σ}) = ∅. Ansonsten beweisen wir sie durch vollständige Induktion über die Anzahl der Iterationen k der While–Schleife. Induktionsanfang (k = 0): 2.3. SEMANTIK VON WHILE 93 In diesem Fall ist vor Betreten der While-Schleife [[ b ]] B (σ) = ff und deswegen [[ while (b) {S} ]](σ) = id. Die Behauptung folgt also direkt aus Lemma 2.41. Induktionsschluß (k → k+1): Sei σ ∈ Ch(I ∧ b) beliebig, und die While-Schleife werde (k+1)-mal durchlaufen. Nach dem ersten Schleifendurchlauf gilt nach Induktionsvoraussetzung σ 0 =df [[ S ]](σ) ∈ Ch(I) und die Schleife wird noch weitere k-mal durchlaufen. Dann gilt aufgrund der Induktionsvoraussetzung [[ while (b) {S} ]](σ 0 ) ∈ Ch(I ∧ ¬b), also insgesamt wie gewünscht: [[ while (b) {S} ]](σ) = [[ S; while (b) {S} ]](σ) = [[ while (b) {S} ]]([[ S ]](σ)) = [[ while (b) {S} ]](σ 0 ) ∈ Ch(I ∧ ¬b). Per Induktionsprinzip folgt die Behauptung. 4. Konsequenzregel: D.h. in der Situation KON S P ⇒P0 {P 0 } S {Q0 } {P } S {Q} Q0 ⇒ Q muß unter der (Induktions–) Voraussetzung (∗) [[ S ]](Ch(P 0 )) ⊆ Ch(Q0 ) gezeigt werden, daß [[ S ]](Ch(P )) ⊆ Ch(Q) gilt. Wegen P ⇒ P 0 und Q0 ⇒ Q gelten jeweils die Zustandsinklusionen (i) Ch(P ) ⊆ Ch(P 0 ) und 0 (ii) Ch(Q ) ⊆ Ch(Q). Dann ergibt sich offensichtlich: (i) (∗) (ii) [[ S ]](Ch(P )) ⊆ [[ S ]](Ch(P 0 )) ⊆ Ch(Q0 ) ⊆ Ch(Q). Per Induktionsprinzip folgt die Behauptung. 2 Bei genauer Betrachtung unseres bisherigen Vorgehens fällt auf, daß z.B. auch {P } S {Q} 94 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK KONS (I b ASS Ixy+ , { I +xy } y = z*y { I x+ } x 0 = x) KOMP { (I b + x 0 = x) } y = z*y { I x } ASS { I x+ } x = x-1 {I + } + + b x 0 = x) } y = z*y; x = x-1 {I }, I { I } S W { ( I ( b))} { Iz } z = y { I } KOMP post pre I z , { I z } Sexp {( I ( b))} ,( I ( b)) KONS post { } { pre} S exp WHILE ASS { (I (x > 0) Abbildung 2.7: Beweisbaum für die totale Korrektheit der Exponentiation für S ≡ while (true) {skip} und beliebige Spezifikationen P und Q partiell korrekt ist. S ist also so etwas wie ein universelles Programm. Tatsächlich erfüllt es sogar {true} S {false}. Diese theoretisch hübsche Eigenschaft ist leider praxisfern, da einem Anwender mit diesem Programm wenig gedient ist. Daher ist in der Praxis entweder eine separate Terminationsbetrachtung oder ein integrierter Beweis der totalen Korrektheit von Nöten. Der integrierte Beweis der totalen Korrektheit kann mit Hilfe folgender modifizierter While–Regel geführt werden: W HILEtot {I ∧ b ∧ (z = t)} S {I ∧ (t < z)}, I ⇒ (t ≥ 0) {I} while (b) {S} {I ∧ ¬b} wobei t ein Ausdruck ist, der von den Variablen, die im Programm S vorkommen, abhängt, und z eine “frische” freie Variable ist, also eine die nicht in I, b, t oder S vorkommt. Die Regel WHILEtot entspricht der alten While–Regel, wobei die Beweisverpflichtung in der Prämisse um eine Terminierungsanforderung erweitert ist. Der Ausdruck t stellt dabei die Terminierungsformel dar. Nach einmaliger Ausführung des Schleifenrumpfes muss der t-Wert echt abnehmen. Die “frische” Variable z stellt sicher, dass der “alte” Wert in der Nachbedingung noch zur Verfügung steht. Die hinzugekommene Implikation I ⇒ (t ≥ 0) in der Prämisse gewährleistet schließlich, dass der t-Wert nie negativ wird. Beispiel 2.47 Betrachten wir erneut das Exponentiationsprogramm aus Beispiel 2.45 so können wir den Beweis leicht zu einem Beweis für die totale Korrektheit von {x = n ∧ y = m ∧ n > 0} Sexp {y = mn } | {z } | {z } pre tot post erweitern. Der formale Beweis ist in Abbildung 2.7 dargestellt. Die Schleifeninvariante I wird direkt aus dem Beweis partieller Korrektheit übernommen. Die Termierungsformel t ist hier denkbar einfach, nämlich direkt durch die Variable x gegeben. 2.3. SEMANTIK VON WHILE 95 Zusätzlich zu den Abkürzungen aus Beispiel 2.45 verwenden wir folgende Notationen: I+ =df (I ∧ x < x0 ) Ix+ =df (I + [x − 1/x]) + Ixy =df Ix+ [y ∗ z/y] 3 Die Implikation I ⇒ (x ≥ 0) in der Prämisse der WHILEtot -Regel ist trivial. Unter den drei Implikationen der Konsequenzregeln sind 2 bereits in Beispiel 2.45 gezeigt worden, so verbleibt einzig der Nachweis einer Implikation: (I ∧ b ∧ x0 = x) ⇒ . . . Wie in Bspl. 2.45 . . . ⇒ (Ixy ∧ x0 = x) ⇒ (Ixy ∧ x − 1 < x0 ) = + Ixy Nicht immer ist die Terminierungsformel so einfach wie im vorangegangenen Beispiel. Wir betrachten daher im folgenden ein weiteres Beispiel, wo eine nichttriviale Terminierungsformel Verwendung findet. Beispiel 2.48 Das Programm Sggt =df while (!(x == y)) {if (x < y) {y = y − x} else {x = x − y}} berechnet den größten gemeinsamen Teiler von x und y. Daher kann die totale Korrektheit von {x = n ∧ y = m ∧ n > 0 ∧ m > 0} Sggt {IsGgT (x, n, m)} gezeigt werden. Dabei drückt für Variablen a, b, c der Ausdruck IsGgT (a, b, c) aus, dass a der ggT von b und c ist. Formal steht der Ausdruck als Abkürzung für folgenden Booleschen Ausdruck der Formelsprache: IsGgT (a, b, c) =df a | b ∧ a | c ∧ (∀ d. d | b ∧ d | c ⇒ d ≤ a) wobei für Variablen a, b der Ausdruck a | b ausdückt, dass a Teiler von b ist. Auch dieser Ausdruck kann wieder in der der Formelsprache spezifiziert werden: a | b =df ∃ c. a ∗ c = b. 3 96 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK ASS ASS Iy ,{Iy }y = y-x {I} Ie Ix ,{Ix}x = x-y {I} CONS CONS {It } y = y-x {I}, {Ie } x = x-y {I} ITE pre {Iif } if (..){...} else {...} {I} WHILE post post pre post I, {I} while (..){...} {Iw } ,I w CONS {pre} while (..){...} {post} It Abbildung 2.8: Beweisbaum für die partielle Korrektheit des ggT-Programmes Es sei hier darauf hingewiesen, dass die so spezifizierte ggT-Eigenschaft in hohem Masse von Quantoren Gebrauch macht und sogar die Schachtelung von all- und existenzquantifizierten Aussagen verwendet. Der formale Hoare-Beweis benutzt in den Konsequenzregeln folgendes Lemma als zahlentheoretische Grundlage der in den Konsequenzregeln verwendeten Implikationen: Lemma 2.49 (IsGgT -Lemma) 1. (x > 0 ∧ x = y ∧ IsGgT (z, x, y)) ⇒ z = x 2. (y > 0 ∧ x > y ∧ IsGgT (z, x, y)) ⇒ IsGgT (z, x − y, y) 3. (x > 0 ∧ y > x ∧ IsGgT (z, x, y)) ⇒ IsGgT (z, y, y − x) 3 Der Beweis ist eine leichte Übungsaufgabe. Zentral für den eigentlichen Hoare-Beweis ist folgende Invariante, die informell ausdr ückt, dass die Modifikation von x und y keine Auswirkungen auf den zu berechnenden ggT haben. I =df (x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) Der zum formalen Hoare-Beweis gehörige Beweisbaum ist in Abbildung 2.8 dargestellt. Dabei werden folgende Bezeichnungen verwendet: 2.3. SEMANTIK VON WHILE 97 pre =df (x = n ∧ y = n ∧ n > 0 ∧ n > 0) post =df IsGgT (x, n, m) I =df (x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) Iwpost =df (¬(¬(x = y)) ∧ I) Iifpre =df (¬(x = y) ∧ I) It =df (x < y ∧ Iifpre ) Ie =df (¬(x < y) ∧ Iifpre ) Ix =df I[x − y/x] Iy =df I[y − x/y] Es bleibt die Rechtfertigung der Implikationen in den Konsequenzregeln. Hier untersuchen wir alle vier relevanten Implikationen: pre ⇒ I . Es gilt: pre ≡ (x = n ∧ y = n ∧ n > 0 ∧ n > 0) [Def. x = n ∧ y = m] ⇒ (x > 0 ∧ y > 0 ∧ (∃ z.IsGgT (z, x, y) ∧ IsGgT (z, n, m))) [Def. I] ≡ I Iwpost ⇒ post . Es gilt: Iwpost = (¬(¬(x = y)) ∧ I) [Einsetzen von I] = (¬(¬(x = y)) ∧ x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) ⇒ (x = y ∧ x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) [Einsetzen x für y] ⇒ (x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, x) ∧ IsGgT (z, n, m))) [Lemma 2.49(1)] ⇒ (∃ z. (z = x) ∧ IsGgT (z, n, m))) ⇒ IsGgT (x, n, m) [Def. post] = post 98 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK Ie ⇒ Ix . Es gilt: Ie = (¬(x < y) ∧ Iifpre ) [Einsetzen von Iifpre ] = (¬(x < y) ∧ ¬(x = y) ∧ I) ⇒ x>y∧I [Einsetzen I] = (x > y ∧ x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) [Lemma 2.49(2)] ⇒ (x − y > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x − y, y) ∧ IsGgT (z, n, m))) = I[x − y/x] [Def. Ix ] = Ix It = (x < y ∧ Iifpre ) [Einsetzen von Iifpre ] = (x < y ∧ ¬(x = y) ∧ I) It ⇒ Iy . Es gilt: ⇒ (x < y ∧ I) [Einsetzen I] = (x < y ∧ x > 0 ∧ y > 0 ∧ (∃ z. IsGgT (z, x, y) ∧ IsGgT (z, n, m))) [Lemma 2.49(3)] ⇒ (x > 0 ∧ y − x > 0 ∧ (∃ z. IsGgT (z, y, y − x) ∧ IsGgT (z, n, m))) [Def. Iy ] = I[y − x/y] = Iy Will man diesen Beweis zu einem Beweis für totale Korrektheit erweitern, muss eine geeignete Terminierungsformel sowohl von x als auch von y abhängen, da in den Schleifeniterationen nur einer der Werte modifiziert wird. Sowohl max(x, y) als auch (x + y) sind hier geeignete Terminierungsformeln, denn der Wert dieser Ausdrücke nimmt in jeder Schleifeniteration echt ab. Wir verzichten an dieser Stelle auf einen formalen Beweis, und überlassen diesen dem Leser als Aufgabe. Als eine zweite Variante einer W HILE-Regel für totale Korrektheit findet man in der Literatur auch: W HILEtot (P (n) ∧ n > 0) ⇒ b, {P (n + 1)} S {P (n)}, P (0) ⇒ ¬b {∃ n. P (n)} while (b) {S} {P (0)} Dabei bezeichnet P (n) ein Prädikat, das von einer freien Variablen n abhängt, die nicht in S vorkommt. Somit ist insbesondere die Prämisse implizit allquantifiziert (∀ n ∈ N. Prämisse). P (n) setzt sich aus der Invarianten I der While–Schleife und aus einer “Terminationsformel”, die von n abhängt, zusammensetzt. P muß nach einmaliger Ausführung 2.3. SEMANTIK VON WHILE 99 des Schleifenrumpfes für n−1 gültig sein. Nachteil dieser Regelvariante ist, dass eine “herabzählende” Terminierungsformel oft nicht problemadädquat ist und nur unter hohem Aufwand zu konstruieren ist. Beispiel 2.50 Das Programm Sdiv =df q = 0; r = x; while (r ≥ y) {r = r − y; q = q + 1 } berechnet in q und r den Quotienten und Rest der ganzzahligen Division von x durch y. Die totale Korrktheit von {x ≥ 0 ∧ y > 0} S {x = q y + r ∧ r < y} kann nachgeweisen werden unter Benutzung der Schleifeninvarianten: Idiv =df (x = q y + r ∧ r > 0) Mit der W HILEtot -Regel ist der Terminierungsausdruck einfach r. Mit der alternativen W HILEtot -Regel ist die Definition eines geeigneten Prädikates P (n) aufwendig. In unserem Beispiel: P (n) =df Idiv ∧ 0 ≤ r − n y < y {z } | Terminierungssformel 3 Satz 2.51 (Korrektheit des Hoare-Kalk üls für totale Korrektheit) Mit Hilfe des Kalküls für die totale Korrektheit (d.h. des Kalküls mit der neuen While– Regel WHILEtot ) lassen sich nur total korrekte Korrektheitsformeln ableiten. 3 Beweis Der Beweis wird wiederum mittels Induktion nach der Länge der Ableitung geführt. Im Induktionsschritt beschränken wir uns auf den Fall der WHILEtot –Regel. W HILEtot {I ∧ b ∧ (z = t)} S {I ∧ (t < z)}, I ⇒ (t ≥ 0) {I} while (b) {S} {I ∧ ¬b} Die anderen Regeln können wie beim Beweis von Satz 2.46 behandelt werden, da die Termination dort kein Problem ist. Unter der (Induktions-)voraussetzung: i) ∀ σ ∈ Ch(I ∧ b ∧ z = t). σ ∈ Def ([[ S ]]) ∧ [[ S ]](σ) ∈ Ch(I ∧ t < z). ii) ∀ σ ∈ Ch(I). [[ t ]]A (σ) ≥ 0. 100 KAPITEL 2. WHILE–PROGRAMME UND IHRE SEMANTIK ist zu zeigen, dass für jeden Zustand σ ∈ Ch(I) gilt: iii) σ ∈ Def ([[ while (b) {S} ]]) iv) [[ while (b) {S} ]]({σ}) ⊆ Ch(I ∧ ¬b) Punkt (iv) ist bereits im Beweis von Satz 2.46 erledigt worden, so dass nur die Terminierung, also Punkt (iii), noch offen ist. Sei also σ ∈ Ch(I). Falls die Bedingung b nicht erfüllt ist, also [[ b ]]B (σ) = ff gilt, terminiert die while-Schleife unmittelbar. Im Falle [[ b ]] B (σ) = tt zeigen wir Behauptung (iii) durch eine verallgemeinerte Induktion über [[ t ]]A (σ). Dabei ist zu beachten, dass wegen Voraussetzung (ii) dieser Wert nichtnegativ ist. Offenbar terminiert die While-Schleife f ür Zustand σ genau dann, wenn sie für Zustand σ 0 =df σ{[[ t ]]A (σ)/z} terminiert. Nach Induktionsannahme terminiert S angesetzt auf σ 0 in einem Zustand σ̃ =df [[ S ]](σ 0 ). Es gilt: (ii) (i) 0 ≤ [[ t ]]A (σ̃) < [[ z ]]A (σ̃) z ∈F / V (S) = [[ z ]]A (σ 0 ) Def . σ 0 = [[ t ]]A (σ) Nach Induktionsannahme der inneren, also der verallgemeinerten, Induktion terminiert dann while (b) {S} angesetzt auf σ̃. Also terminiert auch S; while (b) {S} angesetzt auf σ 0 und somit auch S; while (b) {S} angesetzt auf σ. Wegen [[ while (b) {S} ]](σ 0 ) = [[ S; while (b) {S} ]](σ 0 ) folgt schließlich Behauptung (iii). 2 Vollständigkeit der Methode von Hoare Der Beweis der Vollständigkeit der Methode von Hoare, das heißt die Frage, ob auch alle im Sinne partieller bzw. totaler Korrektheit gültigen Hoare-Tripel im entsprechenden Hoare-Kalküle bewiesen werden können, ist recht kompliziert und gehört deswegen in eine Vorlesung des Haupstudiums. An dieser Stelle sei nur folgendes vermerkt: • Vollständigkeit kann i.allg. nur relativ zu in der Konsequenzregel verwendeten Implikationen gezeigt werden. Man spricht daher von relativer Vollständigkeit. • Legt man die natürlichen Zahlen als semantischen Bereich zugrunde, so ist der HoareKalkül für WHILE–Programme relativ vollständig. 2.3. SEMANTIK VON WHILE 101 Schlußbemerkung zur axiomatischen Semantik Zum Abschluß dieses Kapitels, wollen wir noch die Frage klären, warum die Hoaresche Methode auch axiomatische Semantik genannt wird. Die Semantik eines Programms ist eine Zustandstransformation auf Σ, wie wir sie schon bei der operationellen und denotationellen Semantik kennengelernt haben. Nun ist der Programmentwickler aber nicht an konkreten Zuständen interessiert, sondern an Aussagen P über Zuständen, welche Mengen von Zuständen Ch(P ) charakterisieren. Statt von Zustandstransformationen wird die Semantik eines Programms hier als Funktion auf der Potenzmenge von Σ aufgefaßt. Diese Semantik wird formal mit Hilfe des Hoare–Kalk üls definiert. Da Kalküle aus Axiomen und Regeln aufgebaut sind, wird sie auch axiomatische Semantik genannt. Zu einem gegebenen Programm haben wir mit der operationellen oder denotationellen Semantik Berechnungsvorschriften für die Semantik des Programms zur Verfügung. Wie alle Kalküle liefert der Hoare–Kalkül keine Berechnungsvorschrift für die Programmsemantik und nicht einmal eine Strategie für die Programmverifikation. Das bedeutet, daß sich der Programmentwickler selber überlegen muß, wie er die Korrektheit des Programms nachweisen will. Der Hoare–Kalkül liefert dann eine Möglichkeit, die Verifikationsidee zu formalisieren und ihre Gültigkeit zu beweisen.