Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 Einführung in die funktionale Programmierung 2 Vorlesung Wintersemester 2003 Prof. Dr. Manfred Schmidt-Schauß 1 Kapitel 1 Allgemeines, Literatur, Einführung 1.1 1.1.1 Allgemeines Zeit, Ort und Leistungsnachweis Vorlesung: Di, 10:15-12:00 (SR 11) Do: 10:15-12:00 (SR 11) Übung Di., 8:00-10:00, SR 11 Mitarbeiter Matthias Mann Tutor David Sabel Scheinerwerb: 50% der Punkte aus Lösungen von Übungsaufgaben / Teilnahme an Übungen 1.1.2 Literatur: Jeuring, J., Meijer. E., eds. Advanced functional programming, Lecture Notes in Computer Science 925, Springer-Verlag (1995) Davie, J. An introduction into functional programming using Haskell, Cambridge University Press, (1992) Bird, R., Introduction to Functional Programming using Haskell, Prentice Hall, (1998) Bird, R., Wadler, Ph., Introduction to Functional Programming, Prentice Hall, (1988) Rinus Plasmeijer und Marko van Eekelen Functional programming and parallel graph rewriting, Addison-Wesley, (1993) 2 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 3 Thiemann, Peter Grundlagen der funktionalen Programmierung, Teubner Verlag, (1991) Thompson, Simon , Miranda, The craft of functional programming, Addison Wesley, (1995) Thompson, Simon , Haskell, The craft of functional programming, Addison Wesley, (1999) Davey, B.A., Priestley, H.A., Introduction to Lattices and Order, Cambridge University Press, (1990) Burn, G. Lazy functional languages: Abstract Interpretation and Compilation, Pitman Gunter, C., Semantics of Programming Languages, MIT Press Hinze, Ralf , Einführung in die funktionale Programmiersprache Miranda, Teubner Verlag, (1992) Paul Hudak The Haskell School of expressions, Cambridge University Press (1999) Peter Thiemann Grundlagen der funktionalen Programmierung], Teubner Verlag (1994) G. Winskel The formal semantics of programming languages, MIT Press, Peyton Jones, S. The Implementation of Functional Programming Languages, Prentice Hall, (1987) Handbuch-Artikel: Mosses, P.D. Denotational Semantics, Handbook of Theoretical Computer Science, Vol B, Elsevier, (1990) Gunter, C.A Scott,D.S, Semantic Domains, Handbook of Theoretical Computer Science, Vol B, Elsevier, (1990) Odersky Funktionale Programmierung, Kapitel D5 in Rechenberg, Pomberger: Informatik Handbuch, Hanser-Verlag Klop: Term Rewriting Systems, Handbook of Logic and Computer Science Barendregt, H.P. functional programming and the Lambda-calculus, Handbook of Theoretical Computer Science, Vol B, Elsevier, (1990) Mitchell, J.C., Type systems for programming languages, Handbook of Theoretical Computer Science, Vol B, Elsevier, (1990) Weiterführende Literatur: Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 4 Barendregt, H.P. The Lambda Calculus: - Its Syntax and Semantics, North Holland, (1984) Peyton Jones, S., Lester,D., Implementing Functional Languages, Prentice Hall, 1991 Huet, G. , Logical foundations of functional programming, Addison Wesley, 1990 ODonnel, M. , Equational Logic as a programming language, MIT Press, 1986 Sleep, M.R., M.J.Plasmeijer, M.J., Eekelen, M.C.J.D., Term graph rewriting, Wiley, 1993 Kluge, W., The organization of reduction, data flow, and control flow systems, MIT Press, (1992) Klassische Artikel: Backus: Can programming be liberated from the von Neumann style, Comm ACM 21, 218-227, (1978) Turner, D.A., A new implementation technique for applicative languages, Software Practice & Experience 9, pp. 31-49, (1979) Turner, D.A., Miranda, a non-strict functional programming language with polymorphic types Proc. Conf. on Programming Languages and Computer Architecture, LNCS 201, Springer Verlag, (1985) Milner, Theory of type polymorphism in programming. J. of Computer and System Sciences 17, pp. 348-375, (1978) Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 5 Inhalt (voraussichtlich): • Einführung • Einführung: Semantik von Programmen • Beispiel: Semantik einer einfachen imperativen Sprache IMP • Denotationale Semantik am Beispiel PCF – PCF: Syntax und Semantik – vollständige partielle Ordnungen (cpo’s), stetige Funktionen, Fixpunkte – stetige Konstruktionen: Metasprache • operationale und kontextuelle Semantik von KFPT • Striktheitsanalyse mit Tableaukalkül • Demandanalyse mit Tableaukalkül • Abstrakte Interpretation: Striktheitsanalyse • Nichtdeterministische Sprachen: FUNDIO – let, letrec: Kontextuelle Gleichheit – Ein-/ Ausgabe als nicht-deterministische Erweiterung • Algorithmus zum Testen der Gleichheit • (Terminierungsanalyse) Kapitel 2 Semantik 2.1 Semantik von Programmen Eine formale Semantik (Bedeutung) von Programmen bzw. Programmiersprachen hat den Zweck, das Verhalten eines Programms und seine Wirkung als Funktion, oder seine Auswirkungen auf die Umgebung, formal zu beschreiben. Eine solche Semantik ist unverzichtbar, wenn korrekte Optimierungen, Programmtransformationen, oder Programm- Verifikationen durchgeführt werden sollen. Wir machen folgenden Annahmen: Programm: wir nehmen an, dass Programme eindeutig als Syntaxbäume dargestellt sind. Wir wollen uns nicht mit Mehrdeutigkeiten des Parsens beschäftigen. Die Wirkungsweise eines Programms stellen wir uns so vor, daß die gesamte relevante Umgebung als Eingabe betrachtet wird, die Auswirkung auf diese Umgebung am Ende der Ausführung des Programms als die Ausgabe. Die Abarbeitung soll störungsfrei sein, Fehlfunktionen die zufällig auftreten, werden nicht ins Modell mitaufgenommen. Außerdem soll die Abarbeitung nur durch die Eingabe determiniert sein: Bzw. im nichtdeterministischen Fall sollen die möglichen Abarbeitungen nur durch die Eingabe bestimmt sein. Ein Programm entspricht dann einer Funktion von Eingabe → Ausgabe, oder im nicht-deterministischen Fall einer Relation ⊆ (Eingaben × Ausgaben), oder, wie später bei FUNDIO , wird primär eine Gleichheit von Programmen definiert Formale Semantik (Bedeutung) eines deterministischen Programms: [[.]] : {Programm} 7→ {Funktion} Jedem Programm wird eine Funktion zugeordnet; im nicht-deterministischen Fall i.a. eine Relation. [[.]] : {Programm} 7→ {Relation} Wir werden verschiedene Beschreibungsmethoden einer Semantik behandeln: 6 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 7 • operationale Semantik – Auswertungssemantik (Big-Step Semantik) – Reduktionssemantik – abstrakte Maschine • denotationale Semantik • axiomatische Semantik (nicht in dieser Vorlesung) • transformationsbasierte Semantik Schlechte und unsystematische Vorgehensweisen und Sichtweisen sind folgende: • Ein (geschriebenes)Programm ist ein abkürzende Schreibweise für den zu erzeugenden Maschinenkode. • Jeder weiß doch, was gemeint ist “. ” • informelle Festlegung der Übersetzung jedes Konstruktes • variable Festlegungen, die plattform- bzw. compilerabhängig gemacht werden dürfen. Problematisch daran ist: • Die Korrektheit des Übersetzungsprozesses ist i.a. nur gegen viele detaillierte Übersetzungsvorschriften überprüfbar • Optimierungen und Transformationen die überall“funktionieren, sind nur ” schwer zu finden bzw. als korrekt nachzuweisen. Oder auch: Compiler müssen sich am Maschinenkode orientieren • Die Programme sind mit großer Wahrscheinlichkeit nicht portabel. Teilweise gerät dann die Semantik eher zu einer Spezifikation bzw. Beschreibung des Kompilierprozesses. (“der Compiler ist die Semantik“). Eine bessere Sichtweise ist: Ein Programm beschreibt ein eigenständiges, maschinenunabhängiges Berechnungsverfahren. Dies ergibt an vorteilhaften Eigenschaften: • Die Bedeutung der Quellprogrammiersprache ist unabhängig von der Zielprogrammiersprache / Zielarchitektur beschreibbar. • Idealerweise: Alle Effekte, die ein Programm haben kann, sind durch die Semantik bestimmt. • Ein Kompilers kann (idealerweise) alleine anhand der Semantiken der Quell- und der Zielsprache konstruiert werden 8 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 • Die Korrektheit von vielen Transformationen und Optimierungen kann alleine aufgrund der Semantik der Quellsprache als korrekt nachgewiesen werden. • Korrektheit des Übersetzungsprozesses für alle Plattformen / Betriebssysteme/ Prozessoren kann formal gezeigt werden. Allerdings kann dies auch erfordern, dass zu viel von der Programmierumgebung in die Semantik aufgenommen werden muss. Aspekte, die oft in einer Semantik nicht erfasst sind: Z.B. Ein-Ausgabe, nicht-deterministische Effekte, mit Absicht nicht festgelegte Unbestimmtheit eines Programms, Zufallsgeneratoren, zeitliches Verhalten. 2.1.1 Beschreibungsmöglichkeiten für die Semantik operational (structured operational semantics;SOS): terpreter für die Programmiersprache. baue einen In- Eine operationale Semantik gibt es in verschiedenen Formen: Es gibt eine Variante Auswertungssemantik (“big-step semantics“), d.h. man gibt die Auswertung eines Programms an, wenn man weiß, wie Teile des Programms auszuwerten sind. Ein Interpreter, der sich nach einer big-step Semantik richtet, ist i.a. rekursiv über die Struktur des Programms, wobei möglicherweise die Reihenfolge der Auswertungen nicht ganz genau festgelegt ist. Ergänzend dazu gibt es auch die Möglichkeit, eine Reduktionssemantik zu formulieren, die genau angibt, was als nächstes auszuwerten bzw. welcher Schritt ausgeführt werden muss. Hier muss i.a. in der operationalen Semantik genauer angegeben werden, wo der nächste Befehl (Unterausdruck) ist. Ein Nachteil der big-step Semantik ist folgendes: Es ist möglich, dass der Interpreter rekursiv nach dem nächsten Schritt sucht, und dabei in eine Schleife gerät. Diese Möglichkeit ist nicht explizit in der Beschreibung enthalten. Bei der Reduktionssemantik ist dieser Fall i.a. in der Beschreibung enthalten. Eine weitere Alternative ist die Angabe einer abstrakten Maschine: Zunächst muss man die Datenstrukturen der abstrakten Maschine angeben: i.a. Heap, Stack, Code. Danach muss man für jede Instruktion (jedes Konstrukt) angeben: wie ändert sich der Zustand der abstrakten Maschine?, d.h. welche Änderungen bewirken diese im Heap und Stack und welche Instruktion wird danach ausgeführt. denotational Gebe für jedes Programm P eine formale Beschreibung der Funktion [[P ]] an. D.h. definiere mathematisch, welche Funktion (bzw. Relation) ein Programm definiert. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 9 axiomatisch: Gebe Axiome (bzgl. einer Logik) an, die den exakten Zusammenhang zwischen Programm(teilen) und Zustandsänderung modellieren (Hoare, Dijkstra). transformational Gebe Transformationsregeln an zur Transformation eines Programms in ein anderes (in einer anderen Programmiersprache, bzw. einer mit einfacherer Syntax) I.a. Transformationen auf Konstrukten (z.B. Entzuckerung einer Sprache). Das ist i.a nur ein Teil der Semantik. Eine wichtige Eigenschaft dieser Semantikbeschreibungen ist (bzw. sollte sein:) Kompositionalität: I.a für denotationale Semantik: Die Semantik eines Programms läßt sich aus einfachen Regeln für die Teile eines Programms ermitteln (berechnen). Z.B. [[s + t]] = [[s]] + [[t]]. Beispiele für den Nutzen einer Semantik. • Man hat eindeutige Festlegungen der Wirkung / Auswertung von Programmen. • Man hat ein Werkzeug, um Fehler im Design von Sprachen zu erkennen • Hat man die vollständige operationale Semantik des Instruktionssatzes eines Prozessors, hat man direkt einen Interpretierer ohne den Prozessor selbst zu benötigen. • Spezifiziert man die Semantik einer Programmiersprache und der Zielsprache, so kann man den Übersetzungsprozess formal verifizieren. D.h. man kann den Kompiler im Prinzip formal überprüfen. • Man kann die Übersetzungskette Programmiersprache → Zwischensprache1 → Zwischensprache2 → Maschinensprache anhand der Semantiken für jeden Übergang getrennt überprüfen. 2.2 Semantik der einfachen imperativen Sprache IMP: Als Modellsprache betrachten wir eine sehr einfache imperative Programmiersprache, geben deren Semantik an, und untersuchen einfache formale Verfahren zum Nachweis von Eigenschaften der Semantik und der Sprache. Definition 2.2.1 Syntaktische Objekte und Mengen. 10 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 Zahlen, ganze Zahlen Boolesche Werte Speicherplätze Loc Arithmetische Ausdrücke Boolesche Ausdrücke Befehle n, m True, False X, Y a b c (Symbole) Syntax: Wir nehmen an, dass die Symbole für Speicherplätze alphanumerische Namen sind. arithmetische Ausdrücke: a ::= n | X | a0 + a1 | a0 − a1 | a0 ∗ a1 Boolesche Ausdrücke: b ::= True | False | a0 = a1 | a0 ≤ a1 | ¬b | b0 ∨ b1 | b0 ∧ b1 Befehle c ::= skip | X := a | c0 ; c1 | if b then c0 else c1 fi | while b do c od Zum eindeutigen Parsen kann man Prioritäten der Operatoren verwenden. Beispiel 2.2.2 Einige IMP-Beispielprogramme sind: y := 2: z := 4; x := y + z x := 1; y := 100; while 2.3 y > 0 do x := x*y; y := y-1 od Operationale Semantik: Auswertung von IMP-Ausdrücken Als Zustand bezeichnen wir eine Funktion: σ : Loc → Z, d.h. eine Belegung der Speicherplätze mit Zahlen. D.h., dass wir der Einfachheit halber keine Booleschen Werte speichern, sondern nur Zahlen. Σ Menge der Zustände = {σ | σ : Loc → Z} σ(X) ist der Wert in einem Speicherplatz X. ha, σi Anforderung: a ist im Zustand σ auszuwerten ha, σi → n a wertet im Zustand σ zu n aus. hb, σi → True hb, σi → False Wir wollen jetzt die operationale Semantik von IMP als Relation beschreiben. Diese Art der Beschreibung ist eine sogenannte Auswertungs-Semantik (bigstep semantics). Die Notation [[·]] ist eine n Semantikbeschreibungen übliche Notation: im Inneren steht der Text des Programms, außerhalb sind es eher mathematische Objekte und Operatoren. [[a]] = {(σ, n) | ha, σi → n} ⊆ Σ × Z [[b]] = {(σ, w) | hb, σi → w} ⊆ Σ × B [[c]] = {(σ, σ 0 ) | hc, σi → σ 0 } ⊆ Σ × Σ Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 11 Im deterministischen Fall sind dies Funktionen: [[a]] : Σ → Z [[b]] : Σ → B [[c]] : Σ → Σ Wir schreiben Regeln oft in der Form: Prämissen Konklusion Regeln für arithmetische Ausdrücke: hn, σi → n hX, σi → σ(X) ha0 , σi → n0 ha1 , σi → n1 ha0 + a1 , σi → n0 + n1 ha0 , σi → n0 ha1 , σi → n1 ha0 ∗ a1 , σi → n0 ∗ n1 ha0 , σi → n0 ha1 , σi → n1 ha0 − a1 , σi → n0 − n1 Zu beachten ist hierbei, dass im Ausdruck ha0 + a1 , σi → n0 + n1 , der linke Ausdruck a0 + a1 ein Text ist, bzw. der entsprechende Teil des Syntaxbaumes, während rechts die mathematische Operation + gemeint ist. Falls man nur beschränkte Zahlbereiche hat, dann wäre das rechte + die Addition mit Überlauf auf dem beschränkten Zahlbereich. Regeln für Boolesche Ausdrücke: hTrue, σi → True hFalse, σi → False ha0 , σi → n ha1 , σi → m ha0 = a1 , σi → True wenn n und m gleich sind ha0 , σi → n ha1 , σi → m ha0 = a1 , σi → False wenn n 6= m ist. ha0 , σi → n ha1 , σi → m ha0 ≤ a1 , σi → True wenn n ≤ m ist. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 ha0 , σi → n ha1 , σi → m ha0 ≤ a1 , σi → False 12 wenn ¬(n ≤ m) gilt. hb, σi → True h¬b, σi → False hb, σi → False h¬b, σi → True hb0 , σi → True hb1 , σi → True hb0 ∧ b1 , σi → True hb0 , σi → t1 hb1 , σi → t2 hb0 ∧ b1 , σi → False wenn t1 ≡ False oder t2 ≡ False hb0 , σi → False hb1 , σi → False hb0 ∨ b1 , σi → False hb0 , σi → t1 hb1 , σi → t2 wenn t1 ≡ True oder t2 ≡ True hb0 ∨ b1 , σi → True Die Regeln mit mehreren Prämissen geben keine explizite sequentielle Auswertungsreihenfolge vor. Dies ist in der einfachen Sprache IMP unproblematisch, kann aber in einer komplexeren Programmiersprache problematisch werden, z.B. wenn Boolesche Ausdrücke Rekursionen enthalten können. Das Weglassen der genauen Reihenfolge ist aber gerade der Vorteil einer Auswertungssemantik (big-step semantics). Man kann von zu genauen Angaben abstrahieren, und die Spezifikation hinschreiben. Mit den bisher vorgegebenen Werkzeugen und Mitteln ist es nicht immer möglich, eine genaue Reihenfolge der Auswertung zu spezifizieren. Dazu benötigt man entweder: • eine Angabe der Reihenfolge, in der die Prämissen auszuwerten sind. • die Möglichkeit, formale Parameter für Werte (bzw. Umgebungen) in den Regeln zu verwenden, z.B. (hier t1 ) hb0 , σi → t1 ht1 ∧ b1 , σi → True hb0 ∧ b1 , σi → True hb1 , σi → t1 ht0 ∧ b1 , σi → t0 ∧ t1 • Oder eine Definitionsmöglichkeit der Relation zwischen Auswertungsanforderungen, z.B. hb0 , σi → t1 hb0 ∧ b1 , σi → ht1 ∧ b1 , σi Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 13 Das ist aber eine Formulierungsmöglichkeit, die eher zu einer Reduktionssemantik gehört. Unten werden wir noch Teile einer Reduktionssemantik für IMP angeben. Auswertung von Befehlen Die Schreibweise hc, σi soll zu einer neuen Umgebung auswerten, da c jetzt einen Programmbefehl darstellt. Wir schreiben σ[m/X] für die Umgebung die wie σ ist, aber X auf m abbildet: m wenn Y = X σ[m/X](Y ) = σ(Y ) wenn Y 6= X hskip, σi → σ ha, σi → m hX := a, σi → σ[m/X] hc1 , σi → σ 0 hc2 , σ 0 i → σ 00 hc1 ; c2 , σi → σ 00 hb, σi → True hc1 , σi → σ 0 hif b then c1 else c2 fi, σi → σ 0 hb, σi → False hc2 , σi → σ 0 hif b then c1 else c2 fi, σi → σ 0 hb, σi → False hwhile b do c od, σi → σ hb, σi → True hc, σi → σ 0 hwhile b do c od, σ 0 i → σ 00 hwhile b do c od, σi → σ 00 Dies ergibt eine vollständige Definition einer operationalen Semantik für IMP. Definition 2.3.1 Sei c ein IMP-Befehl und σ ein Zustand. Dann gilt hc, σi →AS σ 0 gdw durch Anwendung der obigen Regeln sich hc, σi → σ 0 herleiten lässt. Die (partielle) Funktion, die c zugeordnet wird, bezeichnen wir mit →c,AS . Für zwei Zustände σ, σ 0 gilt σ →c,AS σ 0 gdw. hc, σi →AS σ 0 . Diese Definition hat zum Ziel, IMP-Programme als Funktionen auf Zuständen zu definieren, wobei diese Definition der Berechnung folgt. Das Ergebnis (der Endzustand) ist gefunden, wenn die Regeln alle abgearbeitet sind (d.h. wenn das Programm terminiert). Wenn dies nicht der Fall ist, dann müssen und werden wir die zugehörige Funktion als undefiniert betrachten. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 14 Beachte: hc, σi → σ 0 gilt nur dann, wenn es eine endliche Herleitung dafür gibt. Beispiel 2.3.2 Wir geben Beispiele für Herleitungen an: Sei c = X := 1 : Y := 2. Wir schreiben abkürzend für den Zustand {X → n, Y → m} das Paar (n, m). hX := 1, (0, 0)i → (1, 0) hY := 2, (1, 0)i → (1, 2) hX := 1; Y := 2, (0, 0)i → (1, 2) Sei c = while X > 1 do X := X − 1 od. Direkt erhält man: hwhile X > 1 do X := X − 1 od, (0, 0)i → (0, 0) und hwhile X > 1 do X := X − 1 od, (1, 0)i → (1, 0) Für andere Zustände erhält man z.B.: hX > 1, (4, 0)i → True hX := X − 1, (4, 0)i → (3, 0) hwhile X > 1 do X := X − 1 od, (3, 0)i → (1, 0) hwhile X > 1 do X := X − 1 od, (4, 0)i → (1, 0) weitere Regelanwendungen sind: hX > 1, (3, 0)i → True hX := X − 1, (3, 0)i → (2, 0) hwhile X > 1 do X := X − 1 od, (2, 0)i → (1, 0) hwhile X > 1 do X := X − 1 od, (3, 0)i → (1, 0) hX > 1, (2, 0)i → True hX := X − 1, (2, 0)i → (1, 0) hwhile X > 1 do X := X − 1 od, (1, 0)i → (1, 0) hwhile X > 1 do X := X − 1 od, (2, 0)i → (1, 0) Diese kann man zusammensetzen zu einer Herleitung von hwhile X > 1 do X := X − 1 od, (4, 0)i →AS (1, 0) Wir behandeln jetzt eine nichttriviale Fragestellung, die global gilt und die sich aus dem Regelsystem beweisen lässt: Ist die Auswertung deterministisch ? Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 15 D.h. gilt: hc, σi → σ 0 , hc, σi → σ 00 ⇒ σ 0 = σ 00 Zunächst gilt: Aussage 2.3.3 Die Auswertungen von arithmetischen und Booleschen Ausdrücken terminiert und ist deterministisch. Beweis. Induktion nach der (syntaktischen) Größe der Ausdrücke. 2 Satz 2.3.4 Die definierte Auswertungsrelation für IMP ist deterministisch. Beweis. Dazu benötigt man Induktion über die Anzahl der Regelanwendungen. Hierzu muss man sich klarmachen, dass das Regelsystem einen Baum aufbaut, dessen Wurzel das Programm ist, und dessen Vater-Tochter Beziehungen durch die Regeln definiert werden. Die Auswertung eines arithmetischen oder Booleschen Ausdrucks betrachten wir hierzu als unmittelbar und als deterministisch, d.h. wir zählen das nicht mit. Induktions-Basis: Für eine einzige Regelanwendung ohne weitere Befehle in den Prämissen, d.h,. an den Blättern des Herleitungsbaumes, gilt: Es kann nur c = skip, c = (X := a), oder c = while b do c od sein, wobei b unter σ zu False auswertet. Der neue Zustand ist in jedem dieser Fälle eindeutig bestimmt. Induktion: 1. if b then c1 else c2 fi: Die zwei Fälle hb, σi ≡ True oder ≡ False schließen sich aus. Die Prämisse (für c1 bzw. c2 ) wurde in weniger Schritten hergeleitet, also ist dort der Zustand eindeutig bestimmt, also auch für ifthen-else 2. while b do c od: Die Fälle hb, si ≡ True und ≡ False schließen sich aus. Hier betrachten wir den Fall hb, si ≡ True. Im diesem Fall muss man zweimal Induktion für die Herleitung in der Prämisse anwenden: Zuerst ist σ 0 eindeutig, dann σ 00 . Damit hat man die Eigenschaft determini” stisch“ auch in diesem Fall nachgewiesen. 2 Beispiel 2.3.5 IMP-Programme haben manchmal keinen Wert, d.h. die Relation hc, σi → σ 0 gilt nie. Somit ist → c, AS die leere Funktion. while True do skip In der Reduktionssemantik unten kann man sagen: terminiert nicht: Wir können eine Äquivalenz ∼ auf IMP-Programmen definieren: 16 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 Definition 2.3.6 Für beliebige IMP-Befehle c1 , c2 sei: c0 ∼ c1 ⇔ ∀σ, σ 0 : hc0 , σi → σ 0 ⇔ hc1 , σi → σ 0 Diese Äquivalenz erfasst genau die Eigenschaft, dass Programme bei gleicher Eingabe den gleichen Effekt im Zustand ergeben. Sie sagt nichts darüber aus, wie das erreicht wurde, oder wie gut bzw. schnell das erreicht wurde. Mit unseren Mitteln kann man zeigen, dass (while b do c od) ∼ (if b then (c; (while b do c od)) else skip fi) Nachweis: Sei σ gegeben. 1. Sei hb, σi → False. Dann: h(while b do c od), σi → σ. Das gleiche gilt für das rechte Programm h(if b then (c; (while b do c od)) else skip fi), σi → hskip, σi → σ. 2. Sei hb, σi → True. Dann gilt: linke Seite: h(while b do c od), σi → σ 00 , wenn hc, σi 0 00 h(while b do c od), σ i → σ rechte Seite: hif b then c; (while b do c od) else skip fi, σi hc; (while b do c od), σi → σ 00 , wenn hc, σi 0 00 h(while b do c od), σ i → σ . → σ0 und → σ0 → und 3. der Fall, dass hb, σi undefiniert ist, tritt nicht auf. Somit gilt die Behauptung. 2.3.1 Reduktionssemantik von IMP Zur Definition einer Reduktionssemantik (small-step semantics) für IMP brauchen wir, wie oben erwähnt, neue Möglichkeiten der Spezifikation der Auswertung: hb, σi → hb0 , σi soll bedeuten: Um b im Zustand σ auszuwerten, werte stattdessen b0 aus, und nehme diesen Wert. Man kann die Regeln der Reduktionssemantik teilweise aus den Regeln der Auswertungssemantik ausrechnen, indem man sie so formuliert, dass die Auswertungs sequenziell ist. Will man exakt sein, hat man trotzdem die Verpflichtung, einen Beweis zu führen, der zeigt, dass die Reduktionssemantik zum gleichen Ergebnis führt. Wir geben nicht alle Regeln an, da einige die gleichen sind wie bei der Auswertungs-Semantik. Zunächst eine Sequentialisierung der AuswertungsSemantik (big-step semantics): ha0 , σi → n0 ha0 + a1 , σi → hn0 + a1 , σi Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 17 ha1 , σi → n1 hn0 + a1 , σi → n0 + n1 Wir geben dies nur für ausgewählte Operatoren an, der Rest ist analog. Die Angabe mit Prämisse/Konklusion ist hier nur gerechtfertigt, da es der gleiche Zustand σ ist. hn0 + n1 , σi → n0 + n1 ha0 , σi → ha00 , σi ha0 + a1 , σi → ha00 + a1 , σi ha1 , σi → ha01 , σi hn0 + a1 , σi → hn0 + a1 , σi Bei der Auswertung von Befehlen ist die Reduktionssemantik etwas anders als die Auswertungs-Semantik. Auch hier geben wir zunächst eine Regelmenge an, die sequentialisiert: hskip, σi → σ ha, σi → m hX := a, σi → σ[m/X] hc1 , σi → σ 0 hc1 ; c2 , σi → hc2 , σ 0 i hc1 , σi → hc01 , σ 0 i hc1 ; c2 , σi → hc01 ; c2 , σ 0 i hb, σi → True hif b then c1 else c2 fi, σi → hc1 , σi hb, σi → False hif b then c1 else c2 fi, σi → hc2 , σi hb, σi → False hwhile b do c od, σi → σ hb, σi → True hwhile b do c od, σi → hc; while b do c od, σi Daraus kann man die Regeln der Reduktionssemantik gewinnen. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 18 Die Regeln für Operatoren sind nicht alle hingeschrieben. Die Regeln mit Voraussetzung kann man immer lesen als sequentielle Verarbeitung. hm + n, σi →RS m + n hs, σi →RS hn, σi hs + t, σi →RS hn + t, σi ht, σi →RS hm, σi hn + t, σi →RS hn + m, σi hskip, σi →RS σ ha, σi →RS ha0 , σi hX := a, σi →RS hX := a0 , σi ha, σi →RS m hX := a, σi →RS σ[m/X] hc1 , σi →RS σ 0 hc1 ; c2 , σi →RS hc2 , σ 0 i hc1 , σi →RS hc01 , σ 0 i hc1 ; c2 , σi →RS hc01 ; c2 , σ 0 i hif True then c1 else c2 fi, σi →RS hc1 , σi hif False then c1 else c2 fi, σi →RS hc2 , σi hb, σi →RS hb0 , σi hif b then c1 else c2 fi, σi →RS hif b0 then c1 else c2 fi, σi hwhile b do c od, σi →RS hif b then c; while b do c od else skip fi, σi Definition 2.3.7 Die Reduktionssemantik von IMP ist definiert als →RS∗ , wobei hc, σi →RS∗ σ 0 gdw. es eine Relationskette gibt: hc, σi →RS hc1 , σ1 i →RS . . . →RS hck , σk i →RS σ 0 . D.h., wenn es in der transitiven Hülle von →RS gilt. Für einen Befehl c ist die Reduktionssemantik →c,RS als partielle Funktion definiert wie folgt: Für zwei Zustände σ, σ 0 gilt σ →c,RS σ 0 , gdw. hc, σi →RS∗ σ 0 . Beachte, dass diese Form der Reduktionssemantik noch Unterreduktionen benötigt: Reduktion von arithmetischen und Booleschen Ausdrücken und bei 19 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 der Sequenz von Befehlen. Bemerkung 2.3.8 In einer funktionalen Programmiersprache hat die Reduktionssemantik meist eine Regel der Art: s→t C[s] → C[t] wobei C eine bestimmte Klasse von Kontexten ist. Damit ist gemeint, dass man Reduktionen irgendwo im Programm durchführen kann. Bei IMP geht das nicht in dieser Weise, da die Reduktionen auch den Zustand mitverändern, und der Zustand nicht im Programm kodiert ist. Man kann formal zeigen, dass diese Umstellung der Semantik durch explizite Angabe der Abarbeitungsreihenfolge mittels einer Reduktionssemantik äquivalent zur Auswertungs-Semantik ist. D.h. es gilt Aussage 2.3.9 Für alle IMP-Programme c gilt: →c,AS = →c,RS . Beweis. Hier soll nur angedeutet werden, wie man den Beweis führen kann. Wir zeigen dies am Beispiel des while-Konstruktes. ∗ Wenn hwhile b do c od, σi →RS σ 00 , dann auch hwhile b do c od, σi →AS σ: Das kann man mit Induktion nach der Anzahl der insgesamt notwendigen Schritte →RS zeigen. Wegen hwhile b do c od, σi →RS hif b then c; while b do c od else skip fi, σi gilt die Induktionsannahme für die rechte Seite. Der Boolesche Ausdruck wertet gleich aus bzgl beider Semantiken. Im False-Fall ergibt sich bei beiden Semantiken die Umgebung σ. Im True-Fall muss man die beiden Semantiken genauer vergleichen. Bzgl. →RS muss zuerst hc; while b do c od, σi ausgewertet werden. Das ergibt, wieder mit Induktion, die gleiche Umgebung σ 00 für beide Semantiken. Umgekehrt nehmen wir an, dass hwhile b do c od, σi →AS σ 00 . Jetzt muss man Induktion über die Größe der Herleitung für →AS machen. Auch hier ergibt sich unter Verwendung der Regeln beider Semantiken die Gleichheit der resultierenden Umgebungen. Die Beziehung zwischen Reduktionssemantik und Auswertungssemantik bezüglich Nichtterminierung ist folgende: Wenn eine Auswertung hc, σi undefiniert ist in der Auswertungssemantik, dann terminiert die →RA∗ -Auswertung nicht. In der Auswertungssemantik würde der rekursiv definierte Interpreter in eine Schleife geraten. Das kann nur passieren bei der Auswertung eines whileAusdrucks. 2.3.2 Semantik-Definition mittels einer abstrakten Maschine Eine abstrakte Maschine kann man definieren, wenn man sehr explizit, aber trotzdem maschinenunabhängig, sagen will, wie Programme einer Programmier- Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 20 sprache auszuwerten sind. Eine abstrakte Maschine kann auch zum formalen Argumentieren verwendet werden, wenn diese nicht zu detailliert definiert ist. Eine abstrakte Maschine zu IMP kann man folgendermaßen definieren: Definition 2.3.10 Die IMP-Maschine hat einen Zustand (E, B, S), bestehend aus • Umgebung E. Genauer: Zuordnung von Variablennamen zu Zahlen. • Aktueller Befehl B oder Ausdruck. Entspricht in etwa dem Programmzähler. • Stack S. Enthält Zahlenwerte, Boolesche Werte und Befehle. Der Start der Maschine erfolgt mit (∅, P, ∅), wobei P das Programm ist, wenn man keine Umgebung als Eingabe zulassen will. Man kann auch mit einer nichtleeren Umgebung starten. Der Ablauf ist zu Ende, wenn keine Regel mehr anwendbar ist. Erfolgreich terminiert die Maschine, wenn B = skip und der Stack leer ist. Ein Fehler kann nur passieren, wenn die abstrakte Maschine auf nichtinitialisierte Variablen zugreift. Als Ausgabe kann man entweder den Wert in B oder die hinterlassene Umgebung E ansehen. Die Regeln sind: (E, (c1 ; c2 ), S) (E, while b do c od, S) (E, if b then c1 else c2 fi, S) (E, X := t, S) → → → → (E[X = a], n, X :=; S) (E, n, X :=; S) (E, skip, c; S) (E, True, [T : c1 , F : c2 ]; S) (E, False, [T : c1 , F : c2 ]; S) → (E[X = n], skip, S) → (E[X = n], skip, S) wenn X nicht in E vorkommt → (E, c, S) → (E, c1 , S) → (E, c1 , S) (E[X = a], X, S) → (E, a, S) (E, s + t, S) (E, n, (+t); S) (E, m, (n+); S) ... (E, s ≤ t, S) (E, n, (≤ t); S) (E, m, (n ≤); S) → (E, s, (+t); S) → (E, t, (n+); S) → (E, m0 , S) wobei m0 = m + n (E, c1 , c2 ; S) (E, b, [T : c; while b do c od, F : skip]; S) (E, b, [T : c1 , F : c2 ]; S) (E, t, X :=; S) → (E, s, (≤ t); S) → (E, t, (n ≤); S) → (E, w, S) wobei w = True wenn m ≤ n, sonst w = False Die Notation E[X = a] soll bedeuten, dass in der Umgebung E die Variable X den Wert a hat. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 21 Zur Erläuterung der Stackeinträge: • c; S soll ein push von c auf den Stack S bedeuten. • [T : c1 , F : c2 ] ist ein Eintrag von alternativen Fortsetzungen, abhängig vom Wert in B. • X := ist der Eintrag eines Update-Markers auf dem Stack. • (+t) Eintrag im Stack, wie nach Auswertung des ersten Terms fortgesetzt werden soll. Es soll dann mit der Auswertung von t fortgefahren werden. • (n+). Wartet darauf, dass die Auswertung beendet wird. Danach erfolgt Operatoranwendung, d.h. n wird addiert. Implizit wird angenommen, dass die abstrakte Maschine die Einträge auf dem Stack so markiert hat, dass diese eindeutig erkennbar sind. Weiterhin nimmt man an, dass der Zahlbereich unbegrenzt ist. Nicht exakt beschrieben ist auch, wie die Maschine erkennt, was in der Umgebung bereits definiert ist. In einer konkreteren abstrakten Maschine müssen diese Annahmen dann explizit gemacht werden. Die IMP-Maschine terminiert erfolgreich, wenn der Stack leer ist. Die IMPMaschine kann auch steckenbleiben, was man normalerweise als Fehler betrachtet: z.B. wenn ein nicht initialisierter Wert in der Umgebung abgefragt wird Obige abstrakte Maschine definiert eine operationale Semantik der Sprache, die für formale Beweise im Prinzip verwendet werden kann, aber ebenso für Berechnungen der Anzahl der Schritte, die für eine Berechnung benötigt werden. Beispiel 2.3.11 Wir betrachten wieder den Befehl while X > 1 do X := X − 1 od und nehmen an, dass die Umgebung vorher für X den Wert 3 hat. Wir geben die Zustände der Maschine an. Wir kürzen den while-Befehle manchmal mit wh. . . ab. 22 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 E X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=3 X=2 X=2 ... 2.4 B S while X > 1 do X := X − 1 od ∅ X>1 [T : X := X − 1; wh . . . ; F : skip] X (> 1); [T : X := X − 1; wh . . . ; F : skip] 3 (> 1); [T : X := X − 1; wh . . . ; F : skip] 1 (3 >); [T : X := X − 1; wh . . . ; F : skip] True [T : X := X − 1; wh . . . ; F : skip] X := X − 1; wh . . . ∅ X := X − 1 while X > 1 do X := X − 1 od X −1 X :=; wh . . . X (− 1); X :=; wh . . . 3 (− 1); X :=; wh . . . 1 (3 −); X :=; wh . . . 2 X :=; wh . . . skip while X > 1 do X := X − 1 od while X > 1 do X := X − 1 od ∅ Lambda-Notation, Lambda-Kalkül Einfach getypter Um die denotationale Semantik von IMP zu beschreiben, brauchen wir etwas Vorbereitung. Dazu wollen wir einige Hilfsmittel einführen bzw. wiederholen Damit kann man Funktionen formal definieren und Anwendung von Funktionen auf Argumente formal handhaben, indem man die Argumentvariable explizit mit einem Lambda (λ) kennzeichnet. Oft ist schon ein bestimmter Rahmen vorgegeben. Z.B. kann man damit Funktionen auf den natürlichen Zahlen beschreiben, wenn schon Funktionen (+, ∗, −, . . .) und Konstanten (die Zahlen) gegeben sind. Syntax des (einfach getypten) Lambda-Kalküls: e ::= x | c | (e1 e2 ) | (λx.e) In λx.e wirkt λ wie ein Quantor: x ist die gebundene Variable und .“ ist ein Trennzeichen; e ist ein Ausdruck, ” der es erlaubt, einen “Funktionswert“ zu berechnen. Teilweise schreibt man auch den Argumentbereich der Variablen dazu: λ(x ∈ N ).e Funktionen mehrerer Argumente zu definieren ist kein Problem: λx.(λy.e) Verwendet man die Lambda-Notation auf Zahlen, dann ist zum Beispiel λx.x+1 eine Funktion, die zu allen Zahlen die Zahl 1 addiert. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 23 Anwendung auf Argumente schreibt man einfach als (f x). Z.B. ist dann: (λx.x+ 1) 1 die Anwendung der Funktion (λx.x + 1) auf die Zahl 1. Eine Anwendung auf zwei Argumente sieht so aus: (((λx.(λy.e)) a) b) Die Berechnung des Ergebnisses einer Anwendung einer Funktion auf Argumente erfolgt durch Einsetzen (β-Reduktion): (λx.x + 1) 1 → (x + 1)[1/x] = 1 + 1 → 2 Diese Berechnung kann man formal definieren durch Einsetzung: (λx.e) a → e[a/x] (in e wird x syntaktisch durch a ersetzt) !! Achtung: das ist nur informell. Eine genauere formale Definition benötigt evtl. eine Umbenennung von Variablen. Syntaktisch benutzen wir Currying (benannt nach Haskell Curry) um mehrstellige Funktionen durch einstellige darzustellen. Syntaktisch ist das eine Konvention zum Ergänzen von Klammern: f a b = ((f a) b) Die Verwendung von λ-Notation ist am verständlichsten, wenn man eine einfach typisierte λ-Notation verwendet: Man hat eine extra Syntax für Typen (ohne Variablen). τ ::= ι | τ1 → τ2 Wobei ι einen Basistyp (z.B. num, Bool, o.ä) darstellt, und τi wieder Typen sind. Den Ausdruck τ1 → τ2 nennt man Funktionstyp. Jede Variable hat einen zugeordneten Typ. Diese kann man beim Lambda angeben λxτ1 .e. Jede Konstante, die bereits gegeben ist, hat ebenfalls einen Typ. Damit ist es auch einfach, den Typ eines Ausdrucks zu berechnen: Die Typisierungsregeln xτ :: τ Typ von Variablen e :: τ2 :: τ1 → τ2 Typ einer Abstraktion e1 :: τ1 e2 : τ1 → τ2 (e2 e1 ) : τ2 Typ einer Anwendung λxτ1 .e Ausdrücke, für die man einen Typ herleiten kann, heißen dann wohlgetypt im einfach getypten Lambda-Kalkül. Bei Typen verwendet man die einfachere, klammerfreiere Schreibweise τ1 → τ2 → . . . → τn für den Typ τ1 → (τ2 → . . . (τn−1 → τn ) . . .). Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 24 Beispiel 2.4.1 Der Typ der Zahlen sei num. Der Typ von +, ∗ sei num → num → num. Dann ist der Typ von (λxnum .(λy num .(x + 1) ∗ y) gerade num → num → num. Wendet man den Ausdruck an, dann ergibt sich für (λxnum .(λy num .(x + 1) ∗ y) 1 2 der Typ num. Wir können auch einen Tupeltyp erlauben: (τ1 , . . . , τn ) Dann benötigt man noch die spezielle Lambda-Notation: λ(x1 , . . . , xn )(τ1 ,...,τn ) .e Man erlaubt im einfach getypten Lambda-Kalkül nur wohlgetypte Ausdrücke. Man kann mit einigem Aufwand zeigen (siehe Literatur, z.B. Gunter: Semantics of programming languages) dass die Auswertung durch Einsetzen terminiert. 1 Die Terminierung entspricht auch zunächst der intuitiven Vorstellung. Da damit aber die Berechnungskraft des einfach getypten λ-Kalküls (bzw. der λ-Notation) eingeschränkt ist, werden wir in späteren Kapiteln folgende Möglichkeiten der Erweiterung betrachten • rekursive Definition mit Kombinatoren • Rekursion mittels eines speziellen Operators • ungetypte λ-Ausdrücke • allgemeinere Typen (polymorphe Typen) 2.5 Denotationale Semantik von IMP A, B, C sind die Denotationen für arithmetische, Boolesche Ausdrücke und Befehle (commands). Wir notieren diese als Funktionen, wobei wir das syntaktische Argument, (das erste), wie allgemein üblich in einer Doppelklammer : [[.]] schreiben. Das zweite Argument ist ein Umgebungsparameter. A : Aexp → Σ → Z arithmetischer Ausdruck → Zustand → Zahl B : Bexp → Σ → B Boolescher Ausdruck → Zustand → Boolescher Wert C : Cexp → Σ → Σ Boolescher Ausdruck → Zustand → Zustand A[[n]] := λ(σ ∈ Σ).n Σ : Zustände n Konstante A[[X]] := λ(σ ∈ Σ).σ(X) X Speicherplatz A[[a1 + a2 ]] := λ(σ ∈ Σ).(A[[a1 ]]σ) + (A[[a2 ]]σ) A[[a1 − a2 ]] := λ(σ ∈ Σ).(A[[a1 ]]σ) − (A[[a2 ]]σ) A[[a1 ∗ a2 ]] := λ(σ ∈ Σ).(A[[a1 ]]σ) ∗ (A[[a2 ]]σ) 1 Die Länge der Berechnung ist als worst-case Komplexität nicht-elementar. D.h. größer als jeder Turm von Exponentialfunktionen. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 25 Wir nehmen für die Booleschen Ausdrücke an, dass in einer Booleschen Algebra die Konstanten True, False definiert sind mit den Operationen ¬ (nicht), ∧ (und), ∨ (oder). Beachte, dass wir die syntaktische Objekte True, False genauso schreiben wie ihre denotationalen Werte. Boolesche Ausdrücke B[[True]] B[[False]] B[[a1 = a2 ]] B[[a1 ≤ a2 ]] B[[¬b]] B[[b1 ∨ b2]] B[[b1 ∧ b2]] := := := := := := := λ(σ λ(σ λ(σ λ(σ λ(σ λ(σ λ(σ ∈ Σ).True ∈ Σ).False ∈ Σ).A[[a1 ]]σ = A[[a2 ]]σ ∈ Σ).A[[a1 ]]σ ≤ A[[a2 ]]σ ∈ Σ).¬(B[[b]]σ ∈ Σ).(B[[b1 ]]σ) ∨ (B[[b2]]σ) ∈ Σ).(B[[b1 ]]σ) ∧ (B[[b2]]σ) Befehle: Wir nehmen an, dass wir eine dreistellige Funktion cond : Bexpr → Σ → Σ → Σ haben, die wie ein if then else“ funktioniert: ” cond True σ 1 σ2 = σ 1 cond False σ1 σ2 = σ2 Die Definitionen unten werden einfacher, wenn wir eine Variante des cond verwenden: condΣ : (Σ → B) → (Σ → Σ) → (Σ → Σ) → (Σ → Σ) die wie ein cond funktioniert, wenn man vorher den Zustand (als Argument) über die Argumente verteilt: (condΣ f g1 g2 ) σ = cond (f σ) (g1 σ) (g2 σ) Beachte, dass das nur geht, weil in IMP die Auswertung Boolescher (und arithmetischer Ausdrücke) den Zustand nicht verändert. Wir definieren noch die identische Funktion: idΣ := λ(σ ∈ Σ).σ Denotationale Semantik-Definitionen Jetzt definieren wir die Programme sofort als Funktionen, nicht als Relationen: C[[skip]] C[[X := a]] C[[c0 ; c1 ]] := λ(σ ∈ Σ).σ := λ(σ ∈ Σ).σ[(A[[a]]σ)/X] := (C[[c1 ]]) ◦ C[[c0 ]]) bzw. λ(σ ∈ Σ).(C[[c1 ]])(C[[c0 ]]σ) C[[if b then c0 else c1 fi]] := λ(σ ∈ Σ).(condΣ (B[[b]]) (C[[c0 ]]) (C[[c1 ]])) σ Bei der Definition der Denotation von while reichen zunächst die Hilfsmittel nicht aus: Ein erster Versuch unter Ausnutzung der Äquivalenz while b do c0 od ∼ if b then c0 ; while b do c0 od else skip fi Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 26 ergibt: C[[while b do c0 od]] := λ(σ ∈ Σ).(condΣ (B[[b]]) (C[[c0 ; while b do c0 od]]) idΣ ) σ Einfacher durch Anwenden von (λx.f x) → f 2 ist: C[[while b do c0 od]] = condΣ (B[[b]]) (C[[c0 ; while b do c0 od]]) idΣ Um die Rekursion direkter zu sehen, kann man die Sequenzregel anwenden: C[[while b do c0 od]] = condΣ (B[[b]]) ((C[[while b do c0 od]]) ◦ (C[[c0 ]])) idΣ Dies ist eine zirkuläre Definition, die uns zunächst nicht erlaubt, auf die Existenz einer “richtigen“ Funktion C[[while b do c0 od]] zu schließen. Methode: wir betrachten partielle Funktionen für die Denotation der Befehle (Zustandsübergänge) : Σ → Σ. • Wir betrachten die rechte Seite der obigen Gleichung für die Denotation von while als Abbildung Γ : (Σ → Σ) → (Σ → Σ) mit Γ(ϕ) := condΣ (B[[b]]) (ϕ ◦ (C[[c0 ]])) idΣ . Die gesuchten Denotation ϕ : Σ → Σ des while-Befehls muss somit die Beziehung ϕ = Γ(ϕ) erfüllen. • Der Einfachheit halber schreiben wir β für B[[b]] und γ für C[[c0 ]]. Dann wird die obige Definition von Γ zu: Γ(ϕ) := condΣ β (ϕ ◦ γ) idΣ ) Als Funktionsgraph geschrieben, ergibt sich: Γ(ϕ) = ∪ {(σ, σ) | wenn β(σ) = False} {(σ, σ 0 ) | wenn β(σ) = True und (σ, σ 0 ) ∈ ϕ ◦ γ} D.h., wir können Γ als Funktion auf Σ × Σ → Σ × Σ auffassen. Wenn wir zwei partielle Funktionen ϕ1 ⊆ ϕ2 haben, dann gilt offenbar Γ(ϕ1 ) ⊆ Γ(ϕ2 ), (Γ ist monoton), denn die rechte Seite kann nur mehr Paare liefern. Jetzt können wir einen Fixpunkt von Γ konstruieren, den wir dann als Denotation des while-Ausdrucks verwenden werden: 2 sogenannten η-Reduktion Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 27 Wir schreiben ∅ für die leere Menge (als Teilmenge von Σ × Σ). Bilde die Folge ∅, Γ(∅), Γ(Γ(∅)), . . .. Der Ausdruck Γn (∅) entspricht der Näherung der while-Denotation: durchlaufe maximal n-mal die while-Schleife und breche dann ab (mit Nichtterminierung). Für diese Folge gilt: ∅ ⊆ Γ(∅) ⊆ Γ(Γ(∅)) ⊆ . . . da Γ monoton ist: ∅ ⊆ Γ(∅) ist offensichtlich. Γn (∅) ⊆ Γn+1 (∅) ⇒ Γ(Γn (∅)) ⊆ Γ(Γn+1 (∅)) also Γn+1 (∅) ⊆ Γn+2 (∅). Damit gilt Γn (∅) ⊆ Γn+1 (∅) ⊆ Γn+2 (∅). Da es sich um Mengen handelt, können wir einfach die Vereinigung bilden: [ ΓF ix := {Γn (∅) | n ≥ 0} Offenbar gilt dann Γ(ΓF ix ) = ΓF ix Dies ist sogar der kleinste Fixpunkt bzgl. ⊆: Sei y ein weiterer Fixpunkt: Γ(y) = y . Dann gilt bestimmt ∅ ⊆ y, also auch Γ(∅) ⊆ Γ(y) = y, Induktion ergibt: Γn (∅) ⊆ y, also auch ΓF ix ⊆ y. Wir schreiben für den kleinsten Fixpunkt von Γ : Fix(Γ) Dann erhalten wir als Denotation des while-Befehls: C[[while b do c0 od]] := Fix(Γ) wobei Γϕ := condΣ (B[[b]]) (ϕ ◦ (C[[c0 ]])) idΣ Damit haben wir eine Denotation erhalten, die nicht mehr rekursiv ist. Diese erfüllt die Kompositionalität. Beispiel 2.5.1 Welche Denotation hat while X = 0 do skip od ? Raten ergibt: f σ = σ, falls σ(X) 6= 0 C[[while X = 0 do skip od]] = Fix(Γ) wobei Γ ϕ := (condΣ (B[[X = 0]])(ϕ ◦ id)id) (condΣ (λ(σ ∈ Σ).σ(X) = 0) ϕ id) Wir starten die Berechnung mit ϕ0 = ∅. Das ergibt ϕ1 = Γϕ0 . ϕ1 σ = Γϕ0 σ = cond (σ(X) = 0) ∅ σ. D.h. ϕ1 = λ(σ ∈ Σ).cond (σ(X) = 0) ∅ σ. ϕ2 = λ(σ ∈ Σ).cond (σ(X) = 0) (ϕ1 σ) σ. Wenn σ(X) 6= 0, dann ergibt das σ, also wie ϕ1 . Wenn σ(X) = 0, dann ergibt das ϕ1 σ D.h. ϕ1 = ϕ2 = λ(σ ∈ Σ).cond (σ(X) = 0) ∅ σ. D.h. φ1 ist bereits der Fixpunkt. Somit besteht φ1 genau aus den Übergängen σ → σ, falls σ(X) 6= 0. Beispiel 2.5.2 Fakultät X := 1, S := 1; while X ≤ n do S = S ∗ X; X := X + 1 od Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 28 Denotation: C[[X := 1, S := 1; while X ≤ n do S = S ∗ X; X := X + 1 od]] σ = C[[S := 1; while X ≤ n do S = S ∗ X; X := X + 1 od]] (σ[1/X]) = C[[while X ≤ n do S = S ∗ X; X := X + 1 od]] (σ[1/X, 1/S]) Da nur X und S vorkommen, vereinfachen wir zu: = C[[while X ≤ n do S = S ∗ X; X := X + 1 od]] {X → 1, S → 1} = = = = C[[S = S ∗ X; X := X + 1]] σ C[[X := X + 1]] (σ[σ(X) ∗ σ(S)/S]) (σ 0 [σ 0 (X) + 1/X] wobei σ 0 = σ[σ(X) ∗ σ(S)/S] σ[σ(X) ∗ σ(S)/S][σ(X) + 1/X] da σ 0 (X) = σ(X) {X → σ(X) + 1, S → σ(X) ∗ σ(S)} D.h. C[[S = S ∗ X; X := X + 1]] = λσ.{X → σ(X) + 1, S → σ(X) ∗ σ(S)} = C[[while X ≤ n do S = S ∗ X; X := X + 1 od]] = Fix Γ mit Γ ϕ = condΣ (B[[b]]) (ϕ ◦ (C [[c0 ]])) id. Γϕ = condΣ (λσ.σ(X) ≤ n) (ϕ ◦ (λσ.{X → σ(X) + 1, S → σ(X) ∗ σ(S)})) id. Fixpunkt: Nachrechnen ergibt: Für ϕ = ∅ : Γ ϕ σ = σ wenn σ(X) > n, sonst undefiniert. Im allgemeinen Fall Übungsaufgabe. Wie machen es von der Notation her etwas einfacher: Betrachte Funktion auf Paaren der Werte von (X, S): fst: ergibt erstes Arg, snd zweites Argument, ϕ : IN × IN → IN × IN. Γ ϕ = condΣ (λσ.fst(σ) ≤ n; ϕ ◦ (λσ.(fst(σ) + 1, fst(σ) ∗ snd(σ)), id) Γ ϕ σ = cond (fst(s) ≤ n; ϕ ◦ (λσ.(fst(σ) + 1, fst(σ) ∗ snd(σ)σ), σ) = cond (fst(σ) ≤ n, ϕ(fst(σ) + 1, fst(σ) ∗ snd(σ)), σ) Sei f der Fixpunkt von Γ. Da dann f = Γ f ist, kann man dies in eine rekursive Definition umsetzen: f σ = if fst σ ≤ n then f (fst(σ) + 1, fst(σ) ∗ snd(σ)) else σ In Pattern-Schreibweise: f (x, s) = if x ≤ n then f (x + 1, x ∗ s) else (x, s) D.h., man erhält als Fixpunkt der while-Schleife eine Funktion, die die Fakultät berechnet, indem sie als Argumente und Resultate Paare verwendet. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 29 Die Gesamtfunktion in Paar-Schreibweise ist: g(x, s) = f (1, 1) f (x, s) = if x ≤ n then f (x + 1, x ∗ s) else (x, s) Die Funktionen können wir auch darstellen als: (x, s) wenn x > n f (x, s) = (n + 1, s ∗ x ∗ (x + 1) ∗ . . . ∗ n) wenn x ≤ n g(x, s) = (1, 1) wenn 1 > n (n + 1, 1 ∗ 1 ∗ (1 + 1) ∗ . . . ∗ n) wenn 1 ≤ n Interessant ist, dass die Eigenschaften der while-Schleife nach der Bestimmung der Denotation mit Methoden des funktionalen Programmierens behandelt werden können. 2.6 Äquivalenz von operationaler und denotationaler Semantik Wir wollen nachweisen, dass die operationale Semantik von IMP äquivalent zur denotationalen ist. Lemma 2.6.1 Für alle arithmetischen Ausdrücke a gilt: A[[a]] σ = n gdw. ha, σi → n Beweis. • A [[n]]σ = n gdw. hn, σi → n für Zahlen n. • (Induktion nach der Größe der Ausdrücke). A[[a1 + a2 ]] σ = (A[[a1 ]] σ) + (A[[a2 ]] σ) = n1 + n2 gdw. ha1 , si → n1 und ha2 , σi → n2 (Induktionshypothese) gdw. ha1 + a2 , σi → n1 + n2 (Definition der operationalen Semantik) andere Operationen kann man analog nachprüfen. 2 Lemma 2.6.2 Für alle Booleschen Ausdrücke b gilt: B [[b]]σ = True gdw. ha, σi → True und B [[b]]σ = False gdw. ha, σi → False. Beweis. Diesen Beweis kann man analog zum zum Beweis für arithmetische Ausdrücke durchführen, wobei man ebenfalls Induktion nach der Größe der Ausdrücke benutzt und auf die Äquivalenz für arithmetische Ausdrücke zurückgreift. 2 30 Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 Satz 2.6.3 Für alle Befehle von IMP: C[[c]]σ = σ 0 gdw. hc, σi → σ 0 Beweis. “⇒“: Sei C[[c]]σ = σ 0 . Dann gilt auch hc, σi → σ 0 : Induktion über die Größe des Ausdrucks c: • C[[skip]]σ = σ gilt ebenso wie hskip, σi → σ. • C[[X := a]]σ = σ[(A[[a]]σ)/X]. Nach obigem Lemma ist (A[[a]]σ) = n gdw. ha, σi → n. Also gilt C[[X := a]]σ = σ[n/X]. Also gilt hX := a, σi → σ[n/X]. • C[[c0 ; c1 ]] σ := (C[[c1 ]]) (C[[c0 ]] σ). Nach Induktion gilt ((C[[c0 ]]) σ) = σ 0 gdw hc0 , σi → σ 0 und (C[[c1 ]])σ 0 ) = σ 00 gdw. hc0 , σ 0 i → σ 00 . Da (C[[c1 ]])((C [[c0 ]]) s) = σ 00 folgt damit nach Definition der operationalen Semantik auch hc0 ; c1 , σi → σ 00 . • C[[if b then c0 else c1 fi]] σ = cond (B[[b]] σ) (C[[c0 ]] σ) (C[[c1 ]] σ). Wenn (B[[b]] σ) = True, dann gilt auch hb, σi → True, nach Induktion. Dasselbe für (B[[b]] σ) = False. Mit Induktion gilt auch: (C[[c0 ]] σ) = σ 0 gdw. hc0 , σi → σ 0 und (C[[c1 ]] σ) = σ 00 gdw. hc1 , σi → σ 00 . Zusammensetzen ergibt die gewünschte Schlussfolgerung: Sei σ 000 = σ 0 wenn (B[[b]] σ) = True, und σ 000 = σ 00 wenn (B[[b]]σ) = False. Dann gilt C[[if b then c0 else c1 fi]] σ = σ 000 gdw. hif b then c0 else c1 fi, σi → σ 000 . • C[[while b do c0 od]] := Fix Γ wobei Γ ϕ := condΣ (B[[b]]) (ϕ ◦ (C[[c0 ]])) id. Γ(ϕ) = ∪ {(σ, σ) | wenn (B[[b]]σ) = False} {(σ, σ 0 ) | wenn (B[[b]]σ) = True und (σ, σ 0 ) ∈ ϕ ◦ (C[[c0 ]])} Wir schreiben θn := Γn (∅). Dann gilt: θn+1 = ∪ {(σ, σ) | wenn (B[[b]]s) = False} {(σ, σ 0 ) | wenn (B[[b]]σ) = True und (σ, σ 0 ) ∈ θn ◦ (C[[c0 ]])} Wir zeigen mit Induktion hwhile b do c0 od, σi → σ 0 . nach n, dass (σ, σ 0 ) ∈ θn ⇒ – n = 0: θ0 = ∅ , deshalb offensichtlich. – n > 0: Angenommen, die Behauptung gilt bereits für n: und sei (σ, σ 0 ) ∈ θn+1 . Dann: ist entweder (B[[b]]σ) = False und σ 0 = σ. Dann mit Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 31 Induktion nach der Größe der Ausdrücke: hb, σi → False und somit hwhile b do c0 od, σi → σ. Ist (B[[b]]σ) = True, dann gibt es σ 00 mit (σ, σ 00 ) ∈ C[[c0 ]] und (σ 00 , σ 0 ) ∈ θn . Nach Induktion über die Größe der Ausdrücke: hc0 , σi → σ 00 . Induktion nach n und die operationale Regel für whileAusdrücke liefert dann: hwhile b do (c0 ; while b do c0 od) od, σi → σ0 . Unsere Überlegung am AnfangS liefert dann auch hwhile b do c0 od, σi → σ 0 . Da Fix Γ = θn , gilt somit: (σ, σ 0 ) ∈ FixΓ ⇒ (σ, σ 0 ) ∈ θn für ein n, also auch hwhile b do c0 od, σi → σ 0 . 2 Die umgekehrte Richtung kann als Übungsaufgabe gemacht werden. Beispiel 2.6.4 Betrachte die while-Schleife (while True do skip od) Wir geben die operationale Semantik an. Es gilt: hTrue, σi → True, hskip, σi → σ, also gilt nach der Definitionsregel für while die Implikation: hwhile True do skip od, σi → σ 0 hwhile True do skip od, σi → σ 0 Damit kann man nichts anfangen, d.h. man kann nichts schließen, und somit gibt es keinen Zustand σ 0 , so dass hwhile True do skip od, σi → σ 0 herleitbar ist. Da dies bedeutet, dass man nichts über den Zustand danach weiß, kann man den Zustandsübergang selbst als leere Funktion betrachten. Dies ist nicht dasselbe wie: hwhile True do skip od, σi → ∅, was Terminierung bedeuten würde. Folgendes Objekt wird mittels der denotationalen Semantik berechnet. C[[while True do skip od]] := Fix Γ wobei Γϕ := condΣ (λσ.True) (ϕ ◦ id) id Zur Bestimmung des Fixpunktes berechnen wir Γ∅σ = cond ((λσ.True) σ)(ϕ ◦ id σ) σ = cond True (∅ σ) σ = ∅ σ D.h. Γ ∅ = ∅ und somit ist der Fixpunkt die leere Funktion. D.h. die nirgendwo definierte Funktion. Somit entspricht eine stets nicht-terminierende while-Schleife einer leeren Funktion. Allgemeiner heißt das: Für einen Zustand σ ist C[[while b do c od]] σ undefiniert gdw. die while-Schleife nicht terminiert. Funktionale Programmierung 2, WS 2003, Semantik von IMP, 28. Oktober 2003 2.7 32 Verallgemeinerungen, weitere Konstrukte in imperativen Sprachen: denotationale Semantik Wir betrachten einen begin-end-Block, der lokale Variablennamen einführt, wobei die Bereichsregeln lexikalisch sind. D.h. der Geltungsbereich einer Variablen ist immer der innerste Block Syntax: Block ::= begin loc1 . . . locn : c end Denotationale Semantik: C[[begin loc1 . . . locn : c end]] σ = ?? Man sieht nach kurzem Nachdenken, dass die bisherige denotationale Semantik von IMP das Problem der Benutzung von nicht-initialisierten Variablen geschickt verschleiert. Hat man Blöcke, sollte man in diesem Fall nicht den evtl. zufälligen Wert aus der globaleren Umgebung nehmen. Es gibt zwei sinnvolle Alternativen: Initial sind die Werte der Variablen vom Typ Integer 0, oder die Benutzung von nichtinitialisierten Werten ist verboten. Fall 1: automatisch initialisiert zu 0: Bei Eintritt in den Block werden die Variablen auf 0 gesetzt, bei Beendigung wieder restauriert: C[[begin X1 , . . . , Xn : c end]] σ := (σ1 [X1 → σ(X1 ), . . . , Xn → σ(Xn ) | Xi ∈ dom(σ)]) wobei σ1 = C[[c]] σ[X1 → 0, . . . , Xn → 0] und dom(σ) = {X ∈ Loc | σ ist auf X definiert}. Fall 2: Fehler bei Benutzung von nichtinitialisierten Variablen. Wir nehmen an, dass ein ⊥ zu den Zahlen hinzugefügt wurde, wobei ⊥ wie ein durchschlagender Fehler wirkt, bzw. wie Nichtterminierung, d.h +, −, ∗ mit einem Argument ⊥ liefern ⊥. Außerdem können wir auch annehmen, dass σ für Speicherplätze, für die es undefiniert ist, ⊥ liefert. Dann ist die denotationale Semantik: C[[beginX1 , . . . , Xn : c end]] σ := (σ1 [X1 → σ(X1 ), . . . , Xn → σ(Xn )]) wobei σ1 = C[[c]] σ[X1 → ⊥, . . . , Xn → ⊥]. Kürzer: (C [[c]] σ[X1 → ⊥, . . . , Xn → ⊥]) [X1 → σ(X1 ), . . . , Xn → σ(Xn )] Erlaubt man auch Marken und Sprungbefehle, so ist die denotationale Semantik so nicht mehr definierbar. Man muss ein Äquivalent des Programmzählers zum Zustand hinzufügen. Hierzu benutzt man sogenannte Fortsetzungen (Continuations). Ein weiteres Programmkonstrukt, das die Semantik verkompliziert, ist die Behandlung von Ein-Ausgabe innerhalb eines Programms. Wir werden dies für die Sprache IMP in dieser Vorlesung nicht vertiefen, aber innerhalb der Vorlesung am Beispiel von FUNDIO demonstrieren.