Einführung in die funktionale Programmierung 2

Werbung
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.
Herunterladen