Kontextuelle Gleichheit, Programmtransformation, Korrektheit

Werbung
Kapitel 5
Kontextuelle Gleichheit,
Programmtransformation,
Korrektheit
In diesem Kapitel soll die Gleichheit von Programmen bzw. Programmfragmenten von Haskell behandelt werden. Wir führen das auf die Gleichheit von KFPProgrammen zurück. Diese Methode baut darauf auf, dass Haskell-Programme
in KFP-Programme übersetzt werden können.
Weiterhin benötigt diese Gleichheit die operationalen Semantik in KFP, d.h.
Kontexte und die Normalordnungsreduktion in KFP. Wichtigstes Unterscheidungsmerkmal zwischen Ausdrücken bzw. theoretische Beobachtung wird die
Terminierung / Nichtterminierung sein. Es kann dann geschlossen werden, dass
Gleichungen die in KFP gelten, auch rückübersetzt werden können. Aber man
kann nicht alle Gleichheiten in Haskell damit erfassen, da es in Haskell weniger
Kontexte als in KFP gibt.
5.1
Die Kernsprache KFP
In diesem Kapitel führen wir die funktionale Kernsprache KFP ein, auf die man
die höheren Konstrukte einer funktionalen Programmiersprache zurückführen
kann, insbesondere KFPT und Haskellprogramme, Diese Kernsprache ist für
Grundlagenbetrachtungen für nicht-strikte (und strikte) funktionale Programmiersprachen geeignet, da sie eine sehr einfache Syntax und wenig Reduktionsregeln hat.
5.1.1
Syntax der funktionalen Kernsprache KFP
Die Kernsprache KFP ist angelehnt an Kernsprachen der Compiler von funktionalen Programmiersprachen und hat möglichst einfache Syntax, und möglichst
1
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
2
wenig vordefinierte Funktionen, Namen und Konstrukte. KFP hat ein schwaches
Typsystem (Programme mit Typfehlern sind möglich).
Syntax: Es gibt Konstantensymbole, die jeweils eine feste Stelligkeit haben.
Diese nennen wir Konstruktoren. Die Anzahl der Konstruktoren sei N , die Konstruktoren seien mit ci , i = 1, . . . , N bezeichnet,
Wir nehmen an, dass es eine Möglichkeit gibt, alle Konstruktoren mit Stelligkeit
anzugeben, ohne dass wir diese Methode näher spezifizieren. Zum Beispiel als
eine Auflistung aller Konstruktoren mit Stelligkeit.
Definition 5.1.1 Eine einfache kontextfreie Grammatik für KFP-Ausdrücke
(Terme, Expressions EXP ) ist:
E
::= V
V sind Variablen
| \V. EXP
wobei V eine Variable ist.
| (E1 E2 )
| (c E1 . . . En )
wobei n = ar (c)
| (case E {P at1 → E1 ; . . . ; P atN +1 → EN +1 })
Hierbei ist P ati Pattern zum Konstruktor i, und
P atN +1 das Pattern lambda.
(P ati → Ei ) heißt auch case-Alternative.
Pat
::=
(c V1 . . . Var (c) ) | lambda
Die Variablen Vi müssen alle verschieden sein.
Wesentlich ist die andere Struktur des jetzt ungetypten case-Konstruktes: Es
gibt nur ein case, und es sind stets alle Konstruktoren als Alternativen vorhanden, ebenso eine weitere Alternative, die mit dem Pattern lambda abgedeckt
wird, und die zum Zuge kommt, wenn der zu untersuchende Ausdruck eine
Abstraktion ist.
Beispiel 5.1.2 Die Funktion, die erkennt, ob eine Liste, die mit den Konstruktoren Nil, Cons aufgebaut wurde, leer ist, kann man schreiben als:
\xs . (case xs {Nil -> True;
....
(Cons y ys) -> False;
;lambda -> bot}
Bei Konstruktoranwendungen und Pattern gibt es feste Stelligkeitsregeln, da es
sich um feste syntaktische Strukturen handelt.
Zur Kernsprache STG des Compilers von Haskell gehört auch noch ein sogenanntes letrec. Lässt man letrec zu, so gewinnt man Effizienz beim Übersetzen und kann auch eine abstrakte Maschine besser und effizienter formulieren.
Die formale Behandlung eines letrec in der Kernsprache ist sehr aufwändig
und komplex. Zudem kann man mit einer kleineren Kernsprache KFP für formale Untersuchungen der wesentlichen Aspekten der operationalen Semantik
auskommen. Wir lassen deshalb letrec in der Kernsprache weg.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
3
Der Nachteil ist, dass man die Anzahl Schritte einer Auswertung, oder die Anzahl der Schritte einer abstrakten Maschine, d.h. die Komplexität eines Programms, nicht in dieser let-freien Theorie behandeln kann, da die ReduktionsAnzahlen in KFP und Haskell zu verschieden sind.
5.1.2
Übersetzung von Haskell und KFPT nach KFP
Wir gehen kurz auf die erweiterte Kernsprache KFPT ein und zeigen, wie man
diese in KFP kodiert.
Man muss nur noch spezifizieren, wie man den typisierten case-Ausdruck nach
KFP übersetzt:
Man übersetzt ein caseT yp einfach als ein KFP-case, wobei die vorhandenen
Alternativen übernommen werden und für die anderen Konstruktoren extraPattern hinzugefügt werden: (pat → bot), ebenso am Ende (lambda → bot).
Hierbei ist bot ein nichtterminierender Ausdruck.
Diese Übersetzung ist natürlich rekursiv auch für die Unterterme durchzuführen.
Die Verwendung von bot statt eines gezielteren Fehlerausgangs ist absichtlich,
denn in KFP werden Fehler, Abbrüche und Nichtterminierung in einen Topf
geworfen und als gleich behandelt. Der Hintergrund ist, dass KFP einen Begriff
der Gleichheit von Programmen und Ausdrücken bereitstellen wird.
Übersetzung von Haskell nach KFP
Die meisten Konstrukte von Haskell kann man dadurch nach KFP übersetzen,
indem man diese zuerst nach KFPT und dann nach KFP übersetzt. Es fehlen
die Übersetzung rekursiv definierter Superkombinatoren nach KFPT und die
Übersetzung von seq und sstrict. Die rekursiven Superkombinatoren werden
wir noch nachholen.
Die Funktionen seq, strict in Haskell müssen direkt nach KFP übersetzt werden:
seq
s t = case s
{p_1 -> t; ... p_{N+1} -> t}
wobei die Pattern neue Variablen enthalten sollen. Die Übersetzung von strict
kann einfach mittels seq gemacht werden, und von seq weiß man, wie es nach
KFP übersetzt wird.
strict
= \f x -> seq x (f x)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.1.3
4
Baumdarstellung von Termen
Die Darstellung von Ausdrücken als markierte geordnete Bäume (Syntaxbäume)
wird oft verwendet, wenn auch nur implizit: Als Beispiel betrachte f (f a b) c
·=
==
==
==
=

c
·<
f
<<
<<
<<
<
a
f
b
Der erste Term in einer Liste von Termen ist die Funktion, der Rest sind die
Argumente:
Man kann eine kompaktere Darstellung wählen:
a

f·
 ===

==

==


=

c
f· @
@@
@@
@@
@
b
Bei Termen als Bäumen werden wir auch die Adressierungsmethode in Bäumen
verwenden:
f·
 ===

1 
==2
==


=

c
f· @
@@
1
@@
2 @@@

a
b
Die Wurzel (mit Markierung f) hat Adresse ε (leeres Wort) Der Knoten mit
Markierung c hat Adresse 2. Die Blätter mit den Markierungen a, und b haben
die Adressen 1.1 und 1.2.
Die Darstellung mit expliziten Anwendungsknoten (mit @ markiert) ist folgendermaßen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
f
f
5.1.4


5
@·
} ???
}
??
}
??
}}
?
}~ }
c
@@
@@
@@
@@
@
@?
~~ ???
~
~
??
~~
??
~~~
@@
b
@@
@@
@@
@
a
Auswertungsregeln
Wir geben der Vollständigkeit halber die komplette operationale Semantik von
KFP an, auch wenn sich diese nur leicht von der operationalen Semantik von
KFPT unterscheidet.
Definition 5.1.3 Ein Wert bzw. eine WHNF (weak head normal form, schwache Kopfnormalform) in KFP ist ein Ausdruck entweder von der Form
1. (c t1 . . . tn ), wobei n = arity(c) und c ein Konstruktor ist (CWHNF), oder
2. eine Abstraktion: λx . e (FWHNF)
Dazu definieren wir zunächst, was Programmkontexte sind:
Definition 5.1.4 Ein Programmkontext (ein Kontext) ist analog zu einem
Ausdruck, der an einer Stelle ein “Loch“ hat, an dem ein Ausdruck eingesetzt
werden kann. Formal definieren wir: Ein Kontext kann sein:
C
::= [] | (e C) | (C e) | (λx . C)
| (c e1 . . . C . . . ear (c) )
| (case C {p1 → t1 ; . . . ; pN +1 → tN +1 })
| (case t {p1 → t1 ; . . . ; pi → C; . . . ; pN +1 → tN +1 })
wobei e ein Ausdruck ist.
Allerdings gilt in Kontexten die freie Umbenennbarkeit von Variablen nur für
die Unterausdrücke, die das Loch nicht enthalten.
Wenn das Loch in einem Gültigkeitsbereich einer Variablen ist, dann wird der
Kontext als syntaktisch gegebenes Programm angesehen, in dem man an der
Loch-Stelle andere Ausdrücke einsetzen kann, auch solche, die freie Variablen
haben, und diese freien Variablen dann nach dem Einsetzen gebunden sind, d.h.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
6
eingefangen wurden. D.h. es tritt der Effekt auf, dass die Namen von gebundenen Variablen eine Rolle spielen, sofern das Loch innerhalb des Rumpfs einer
Abstraktion ist. Z.B. ist der Kontext λx . [] vom Kontext λu . [] verschieden,
da beim Einsetzen von x jeweils andere Ausdrücke entstehen.
Beispiel 5.1.5 Damit sind als Kontexte z.B. definiert:
• [·] : der leere Kontext. Setzt man einen Ausdruck e in den leeren Kontext
ein, so ergibt sich e selbst.
• (x ([·] y)). Einsetzen von t in den Kontext ergibt (x (t y))
• (case (x [·]) {p1 → t1 ; . . . ; pN +1 → tN +1 }). Einsetzen von e in den
Kontext ergibt (case (x e) {p1 → t1 ; . . . ; pN +1 → tN +1 })
• Mit C := λx.([·] x) ergibt sich C[x] = λx.(x x)
Beachte, dass innerhalb eines Pattern kein Loch sein kann, ebenso ist der Kontext λ · .e nicht möglich.
Wir werden den Begriff der Gleichheit und Ungleichheit von Ausdrücken noch
formaler spezifizieren.
Definition 5.1.6 Auswertungsregeln (Reduktionsregeln), wobei (Case) zwei
Regeln hat.
Beta
((λx.t) s)
t[s/x]
Case
(case (c t1 . . . tn ) {. . . ; c x1 . . . xn → s; . . .})
s[t1 /x1 , . . . , tn /xn ]
(case (λx.t) {. . . ; lambda → s})
s
Wir nennen das die unmittelbare Reduktion von Ausdrücken. Wir unterscheiden im folgenden zwischen allgemeiner Reduktion (oder kurz: Reduktion) und
unmittelbarer Reduktion, wobei die allgemeine Reduktion auch die unmittelbare
Reduktion eines Unterausdrucks umfasst.
Die Auswertungsregeln sollen in allen Programmkontexten verwendet werden
dürfen. D.h. Wenn s → t in einem Schritt reduziert, dann gilt auch C[s] → C[t]
für jeden Programmkontext C.
Die Reduktionsrelation schreiben wir als s → t, wenn die (allgemeine) Reduktion
in einem Schritt erfolgt. Die transitive bzw. reflexiv-transitive Hülle schreiben
+
∗
wir als s −
→ t bzw. s −
→ t.
Der Begriff des Redex (reducible expression) kann jetzt definiert werden: Wenn
in C[s] das s unmittelbar reduziert werden kann, dann ist s (zusammen mit
seiner Position) ein Redex in C[s].
Man definiert die Normalordnungs-Reduktion (normal-order-Reduktion,
Standard-Reduktion), wozu man Reduktionskontexte braucht.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
7
Definition 5.1.7 Reduktionskontexte sind:
R
::= [] | (R e) | (case R {p1 → t1 ; . . . ; pN +1 → tN +1 }
Jetzt ist die Normalordnungs-Reduktion diejenige, die immer in einem Reduktionskontext den Ausdruck unmittelbar reduziert:
Definition 5.1.8 Sei R[s] ein Ausdruck, so dass R ein Reduktionskontext ist,
wobei s keine WHNF ist, und s reduziert unmittelbar zu t:
Dann ist R[s] → R[t] die Ein-Schritt-Normalordnungsreduktion (Standardreduktion, normal order reduction) von R[s].
Der Unterterm s zusammen mit seiner Position wird auch Normalordnungsredex genannt. Die Reduktionsrelation wird mit dem Marker n versehen. Analog
n,+
wie oben bezeichnen wir die transitive und reflexiv-transitive Hülle mit −−→ bzw.
n,∗
−−→.
n,∗
Die Relation −−→ bezeichnet man auch als Normalordnungsrelation oder als Auswertung eines Terms.
Wenn ein geschlossener Term t unter Normalordnung zu einer WHNF reduziert,
dann sagen wir, t konvergiert bzw. terminiert und bezeichnen dies mit t⇓. Anderenfalls und falls t geschlossen ist, divergiert t, bezeichnet mit t⇑. Wenn t offen
ist, dann definieren wir t: divergiert (t⇑), falls t eine Normalordnungsreduktion
hat, die unendlich ist, oder die Normalordnung endet mit einem Term der Form
R[t0 t00 ], wobei t0 eine Konstruktoranwendung ist und R ein Reduktionskontext.
Der Fall R[x] bleibt dabei offen.
Wir sagen, t hat eine WHNF, wenn t zu einer WHNF reduziert. Die WHNF, zu
der t unter Normalordnung reduziert, ist eindeutig, aber es gibt i.a. viele WHNFs
zu denen ein Term t reduzieren kann. Z.B. t ≡ (cons ((λx.x) True)) Nil) ist
selbst in WHNF, aber (cons True Nil) ist ebenfalls eine WHNF zu t.
Alternative Definition des Normalordnungsredex:
Sei R das Label: Starte mit tR und wende die Regeln solange an, bis keine
Anwendung mehr möglich ist.
C[(s t)R ]
→ C[(sR t)]
C[(case s alts)] → C[(case sR alts)]
Normalordnungsreduktion; mit R-Label definiert:
Beta
((λx.t)R s)
t[s/x]
Case
(case (c t1 . . . tn )R {. . . ; c x1 . . . xn → s; . . .})
s[t1 /x1 , . . . , tn /xn ]
(case (λx.t)R {. . . ; lambda → s})
s
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
8
Damit haben wir alle zur Auswertung von Ausdrücken notwendigen Begriffe
definiert und können jetzt Ausdrücke auswerten.
n
Beispiel 5.1.9 ((λx.x) Nil) −
→ Nil
n
n
((λx.λy.x) s t) −
→ ((λy.s) t) −
→ s.
Lemma 5.1.10 Es gilt auch in KFP:
• Jede unmittelbare Reduktion in einem Reduktionskontext ist eine Normalordnungsreduktion.
• Der Normalordnungsredex und die Normalordnungsreduktion sind eindeutig.
• Eine WHNF hat keinen Normalordnungsredex und erlaubt keine Normalordnungsreduktion.
Beweis. Das erkennt man an der Definition der Reduktionskontexts und der
Normalordnungsreduktion.
2
5.2
Kontextuelle Gleichheit von Ausdrücken in
KFP
Gleichheit von Ausdrücken s, t kann man annehmen, wenn s und t sich gleich
verhalten (Verhaltensgleichheit). Da die Erkennung des Verhaltens ein formales Beobachten erfordert, benutzen wir das Kriterium der Terminierung bzw.
Nichtterminierung als Ergebnis der theoretischen Beobachtung. Natürlich kann
man Nichtterminierung nicht praktisch bzw. effektiv beobachten, insofern ist
das Beobachtungskriterium nicht effektiv. Die formale Definition hilft aber beim
Erkennen und Nachweisen der Korrektheit von Programmtransformationen, wie
wir sehen werden.
Man sieht (offene und geschlossene) Ausdrücke s, t als gleich an, wenn Programme ihr Terminierungsverhalten niemals ändern, wenn man s durch t ersetzt.
Dieses Kriterium erscheint zunächst schwach, aber da man alle Programme und
alle Stellen innerhalb der Programme ausprobieren kann, ergibt sich ein starkes
Kriterium.
Man kann im Prinzip auch andere Formen von Tests nehmen. Wenn man Beobachten im engeren Sinne auslegt, könnte man auch die Effizienz eines Programmes als Kriterium nehmen (Anzahl der Schritte). Das wäre eine stärkere
Forderung, die wir aber hier nicht betrachten, da die kontextuelle Ordnung darauf nicht zugeschnitten ist, und sich dann fast nichts mehr nachweisen lässt.
Man definiert zunächst eine Approximationsordnung s ≤c t, die bedeutet, dass
t besser terminiert als s, oder, dass s weniger Information liefert als t.
Definition 5.2.1
(Kontextuelle Approximation) Seien s, t KFP-Ausdrücke
und C[.] ein Kontext.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
9
s ≤c t gdw. ∀C[] : C[s]⇓ ⇒ C[t]⇓
s ∼c t gdw s ≤c t ∧ t ≤c s
Die Ordnung ≤c nennen wir kontextuelle Approximation, und ∼c kontextuelle
Gleichheit.
Die Gleichheitsrelation kann man auch interpretieren als:
Terme sind gleich, wenn man sie mit keinem Experiment unterscheiden kann.
Das Experiment ist: Einsetzen in einen Kontext und Terminierung beobachten.
Ein großer Vorteil dieser Definition ist, dass man sich ganz auf die definierte
Sprache und die Auswertung zurückziehen kann, ohne die Hilfe einer externen
Semantik in Anspruch zu nehmen. Ein weiterer, dass man viele Schlüsse und
Methoden unabhängig von der Sprache entwickeln kann.
Ein kleiner Nachteil ist, dass diese Definition der Gleichheit von Ausdrücken
vom Sprachumfang abhängt. Die Gleichheit kann sich ändern, wenn man die
Sprache verkleinert oder erweitert.
Beispiel 5.2.2 Mit
∼c
kann
man
z.B.
(S
0)
von
True
unterscheiden,
indem
man
den
Kontext
case .
{True -> True; False -> True: ... ;(S x) -> bot: ...}
verwendet.
case (S 0) {True -> True; False -> True; ... ;(S x) -> bot; ...}
terminiert nicht; aber case True {True -> True; False -> True; ...}
ist reduzierbar zu True.
Eine wichtige und (im Gegensatz zu anderen Definitionen) leicht zu zeigende
Eigenschaft ist, dass sich durch Ersetzung eines Unterausdrucks e durch einen
kontextuell gleichen Ausdruck e0 ein Programm in ein kontextuell äquivalentes
verwandelt.
Aussage 5.2.3 ≤c und ∼c sind stabil gegen Einsetzung in Kontexte und ≤c ist
eine Präordnung, während ∼c eine Äquivalenzrelation ist. Zusammengefasst:
≤c ist eine Präkongruenz und ∼c ist eine Kongruenzrelation auf Ausdrücken.
Beweis. Offensichtlich ist ≤c transitiv und ∼c ist eine Äquivalenzrelation. Wir
zeigen, dass ≤c stabil gegen Einsetzung in Kontexte ist. Sei dazu s ≤c t. Wir
zeigen dass für einen beliebigen Kontext C: C[s] ≤c C[t] gilt. Sei D ein Kontext
und sei D0 := D[C[]]. Aus s ≤c t folgt dann dass D[C[s]]⇓ ⇒ D[C[t]]⇓. Da das
für alle Kontexte D gilt, folgt die Behauptung.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
10
Ein Vorteil dieser Herangehensweise ist die Maximalität der kontextuellen
Gleichheit. Bei Definition einer zu schwachen Gleichheit ∼weak , d.h. die zuviele Terme unterscheidet, kommt es vor, dass man man bei bestimmten Programmtransformationen dann doch für deren Korrektheit argumentiert, obwohl
die Programme nach der Transformation verschieden sind (unter ∼weak ),. Diese
sind aber i.a. noch gleich unter ∼c .
Es gilt, dass ∼c maximale Relation ist mit folgenden Eigenschaften:
• ∼c ist Kongruenz: D.h. Äquivalenzrelation und kompatibel mit Kontexten.
• s → t =⇒ s ∼c t
• λx.s 6∼c (c . . .)
• c1 . . .) 6∼c (c2 . . .)
• bot 6∼c (c . . .)
• bot 6∼c λx.s
Der Nachweis der Maximalität ist relativ einfach: Sei ∼max diese Relation.
• ∼c hat alle diese Eigenschaften, also ist ∼c ⊆ ∼max .
• Sei s ∼max t und C ein Kontext, und sei C[s]⇓. Da die Relation ∼max
kompatibel mit Kontexten ist, gilt C[s] ∼max C[t]. Aus C[s]⇓ folgt, das
C[s] ∼max s0 und s0 ist eine Abstraktion oder eine Konstruktoranwendung. Damit gilt aber auch, dass C[t] ∼max s0 . Wenn die (eindeutige)
Normalordnung von C[t] nicht konvergiert, dann gilt C[t] ∼max bot, Das
ergibt einen Widerspruch, also gilt C[t]⇓. Das gilt für alle Kontexte, also
gilt s ∼max t =⇒ s ∼c t.
5.3
KFP: Invarianz der Terminierung, Standardisierungssatz
Wir behandeln jetzt die Sprache KFP etwas formaler. Das Ziel dieses Kapitels
ist der Nachweis, dass die kontextuelle Äquivalenz erhalten bleibt beim Reduzieren. Dazu wird als Hilfsmittel gezeigt, dass Terminierung von Ausdrücken
erhalten bleibt, wenn man Reduktionsregeln anwendet. Genauer, dass im Falle
t → t0 die Terminierung sich nicht verändert; d.h. t⇓ ⇔ t0 ⇓. Diese erfordert
die Betrachtung beider Implikationen t⇓ ⇒ t0 ⇓ und t⇓ ⇐ t0 ⇓, bei deren Beweis
jeweils verschiedene Methoden verwendet werden.
Eine verwandte, aber doch leicht andere Aussage (Standardisierungssatz), ist der
Nachweis, dass jeder Ausdruck, der sich mit irgendeiner Folge von Reduktion
mit den Kalkülregeln auf eine WHNF reduzieren lässt, auch eine terminierende
no-Reduktion hat.
Das ganze ist technisch etwas aufwändig, aber doch elementar. Da diese Aussagen die Basis für die Korrektheit von Programmtransformationen sind, lohnt es
sich auch, diese etwas genauer durchzugehen.
11
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Zuerst eine informelle Begründung, warum man mit einfacher Induktion nicht
zeigen kann, dass s → t =⇒ (s⇓ ⇔ t⇓). Das Problem liegt in der Verdopplung
der Beta-Reduktion:
/ (λx.(x x)) a
(λx.(x x)) ((λy.y)a)
no
no
/aa
/ ((λy.y)a) a
((λy.y)a) ((λy.y)a)
Mit Induktion nach der Länge der Normalordnung kann man damit, nachdem
man alle Überlappungsfälle analysiert hat, zumindest die Richtung s → t =⇒
(s⇓ =⇒ t⇓) zeigen. Die andere Richtung ist das Problem. Denn das obere Bild
hat Varianten: es könnte auch
/ (λx.(x x)) a
(λx.(x x)) ((λy.y)a)
no
((λy.y)a) ((λy.y)a)
no
no
/ ((λy.y)a) a
/aa
Damit hat man als allgmein Vertauschungsregel nur:
/t
s
no,∗
no
∗
s0 _ _ _/ t0
die keine Induktion mehr zulässt, da man keine oberen Schranken für die Anzahl
der Reduktionen nach Vertauschung hat.
Für die Induktion gibt es eine Lösung, die wir aber für beide Richtungen verwenden:
Wir benötigen dafür die sogenannte 1-Reduktion (siehe [?]), deren Bedeutung
die parallele 1-Schritt-Reduktion ist. D.h. zwei Terme s, t stehen in der 1Relation, wenn s parallel auf t reduzierbar ist. Hierbei ist gemeint, dass man
irgendeine Untermenge der Redexe von s gleichzeitig (parallel) reduziert.
Dieser Parallelitätsbegriff muss formal definiert werden, da es nicht ganz offensichtlich ist, wie er gemeint ist. Betrachte z.B.
Beispiel 5.3.1 (λx.x x)((λy . y)a). Es gibt zwei Redexe die man unabhängig
voneinander reduzieren kann. Z.B. ist dieser Term parallel reduzierbar zu: a a.
Aber: der Term (λx.x x)(λy . y) ist nur 1-reduzierbar zu (λy . y) (λy 0 . y 0 ), und
nicht zu (λy 0 . y 0 ). Diese Reduktionen sind sequentiell, denn der zweite Redex
entsteht erst durch die Reduktion.
Definition 5.3.2 Sei →1 die folgende Relation auf Ausdrücken:
• s →1 s
• Wenn s →1 s1 und t →1 t1 , dann (s t) →1 (s1 t1 ).
12
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
• Wenn s →1 s1 , dann (λ x . s) →1 (λ x . s1 ).
• Wenn si →1 s0i und t →1 t0 , dann
case t {(c x1,1 . . . x1,n1 ) → s1 ; . . . ; (c xm,1 . . . xm,nm ) → sm }
→1 case t0 {c x1,1 . . . x1,n1 → s01 ; . . . ; (c xm,1 . . . xm,nm ) → s0m }.
• Wenn s →1 s1 , t →1 t1 , dann (((λ x . s)) t) →1 s1 [t1 /x]
• Wenn si →1 s0i , t →1 t0 , dann
(case (c s1 . . . sn ){. . . ; (c x1 . . . xn ) → t; . . .}
→1 t0 [s01 /x1 , . . . , s0n /xn ].
• Wenn t →1 t0 , dann
(case (λx.s){. . . ; lambda → t; . . .}
→1 t0 .
Die 1-Relation kann aufgelöst werden in eine Folge von normalen Reduktionen:
∗
Lemma 5.3.3 Wenn s →1 t gilt, dann auch s −
→ t.
Beweis. Das kann man mit Induktion nach der Struktur des Terms s zeigen.
Als Beispiel zeigen wir wie das im Fall der Beta-Reduktion geht: Angenommen,
s = ((λx.s1 ) s2 ) und s1 →1 t1 , s2 →1 t2 und t = t1 [t2 /x]. Dann gilt mit
∗
∗
∗
∗
Induktion, dass s1 −
→ t1 , s2 −
→ t2 , also auch ((λx.s1 ) s2 ) −
→ ((λx.s1 ) t2 ) −
→
∗
((λx.t1 ) t2 ) → t1 [t2 /x], also insgesamt s −
→ t.
2
no
Lemma 5.3.4 Sei t ein (evtl. offener) Ausdruck. Wenn t −→ s und t →1 t0 ,
dann ist entweder s →1 t0 , oder es existiert ein Term s0 , so dass s →1 s0 , und
no
t0 −→ s0 .
1
t
1
no
s
/ t0
@
/ t0
no
no
1
s _ _ _/ s0
t
1
Beweis. Dies zeigt man mit Induktion über die Struktur des Ausdrucks; Genauer
über die Tiefe des Normalordnungsredex: Dazu braucht man eine Fallunterscheidung:
• Sei t ≡ t1 t2 und t1 ist keine Abstraktion. Dann ändert die Normalordnungsreduktion den Ausdruck t2 nicht. Für den Ausdruck t1 ist die
no
Tiefe des Normalordnungsredex kleiner, also gilt mit t1 −→ s1 , auch:
no
t1 t2 −→ s1 t2 . Für t1 t2 →1 t01 t02 gilt t1 →1 t01 und t2 →1 t02 . Mit
Induktion gibt es jetzt zwei Fälle:
13
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
– s1 →1 t01 . Dann gilt auch s1 t2 →1 t01 t02 .
1
t1 t2
no
x
s1 t2
1
x
x
/ t01 t02
x;
no
– Es gibt ein s01 mit s1 →1 s01 und t01 −→ s01 .
no
Zusammengesetzt ergibt das: s1 t2 →1 s01 t02 und t01 t02 −→ s01 t02 .
/ t01 t02
no
no
1 _/ 0 0
_
_
s1 t2
s1 t2
t1 t2
1
• t ≡ t1 t2 und t1 ist eine Abstraktion λx . t3 . Dann ist die Normalordno
nungsreduktion gerade t1 t2 −→ t3 [t2 /x], wobei wir die Konvention über
disjunkte (gebundene) Variablennamen annehmen. Es gibt zwei Fälle für
die 1-Relation:
Wenn t1 t2 →1 t01 t02 , dann können wir auch schreiben (λx . t3 ) t2 →1
(λx . t03 ) t02 mit t3 →1 t03 , t2 →1 t02 . Damit gilt t3 [t2 /x] →1 t03 [t02 /x] und
no
(λx . t03 ) t02 −→ t03 [t02 /x].
Wenn (λx . t3 ) t2 →1 t03 [t02 /x], dann gilt t3 [t2 /x] →1 t03 [t02 /x], und wir
haben die Behauptung gezeigt.
• Wenn t ≡ case t1 {alt1 . . . altn }, und dieser Ausdruck ist kein Normalordnungsredex, dann kann man Induktion über die Struktur wie im ersten
Fall machen.
• Wenn t ≡ case t1 {alt1 ; . . . ; altn } und der Ausdruck ist ein
Normalordnungsredex,
dann
ist
die
Normalordnungsreduktion
no
case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .} −→ tn+1 [t1 /x1 , . . . tn /xn ].
Die 1-Reduktion kann sein: case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) →
tn+1 ; . . .} →1 case (c t01 . . . t0n ) {. . . ; (c x1 . . . xn ) → t0n+1 ; . . .}. In diesem
Fall ist die Vertauschbarkeit analog zu den anderen Fällen.
Wenn die 1-Reduktion direkt den case-Ausdruck reduziert:
case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .} →1 t0n+1 [t01 /x1 , . . . , t0n /xn ],
dann ist auch: tn+1 [t1 /x1 , . . . , tn /xn ], →1 t0n+1 [t01 /x1 , . . . , t0n /xn ].
Analog sind die Fälle, in denen der Ausdruck von der Form
(case (λx.t) {. . .}) ist.
2
14
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
no,k
Aussage 5.3.5 Sei t ein (evtl. offener) Ausdruck. Wenn t −−−→ t2 und t2 ist
no,k0
eine WHNF, und t →1 t0 . Dann gilt t0 −−−→ t02 , t2 →1 t02 , wobei k ≥ k 0 und t02
eine WHNF ist. Somit terminiert dann die no-Reduktion von t0 .
/ t0
0
no,k
no,k
1
t2 _ _ _/ t02
t
1
≤k
Beweis. Induktion nach k, der Anzahl der Normalordnungsreduktionen. Im Basisfall muss man nur noch beachten, dass aus t →1 t0 und t ist WHNF folgt,
dass t0 ebenfalls eine WHNF ist.
no,k−1
no
Im Falle k > 0 zerlegen wir die no-Reduktion in t −→ t1 −−−−−→ t2 . Nach
Lemma 5.3.4 gibt es zwei Fälle:
• t1 →1 t0 . Induktion zeigt die Existenz einer WHNF t02 mit t2 →1 t02 und
no,k0
t0 −−−→ t02 und k − 1 ≥ k 0 . Dann gilt auch k ≥ k 0 und die Behauptung des
Lemmas ist gezeigt.
no
• Es gibt ein t01 mit t0 −→ t01 und t1 →1 t01 . Induktion nach k zeigt dann die
no,k0 −1
Existenz einer WHNF t02 mit t01 −−−−−→ t02 und t2 →1 t02 und k−1 ≥ k 0 −1.
no,k0
Zusammensetzen der Diagramme ergibt t0 −−−→ t02 und t2 →1 t02 und
k ≥ k0 .
1
t
1
no
1
t2
t
no,k0
0
? t2
t1
no,k−1
/ 0
?t
1
/ t0
no
t1
1
no,k−1
t2
1
no
/ t01
no,k0 −1
/ t0
2
2
Da jede 1-Schritt-Reduktion auch als 1-Relation darstellbar ist, folgt unmittelbar:
Satz 5.3.6 Sei t ein (evtl. offener) Ausdruck. Wenn t⇓, und t → t0 mit einer
anderen Reduktion. Dann gilt auch t0 ⇓.
Jetzt fehlt noch die Richtung: (t → t0 ∧ t0 ⇓) ⇒ t⇓. Die Idee hier ist, eine
Reduktion zur WHNF, die aus Normalordnungsreduktionen und 1-Reduktionen
besteht, in eine Normalordnungsreduktion zu verwandeln.
15
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Definition 5.3.7 Sei t ein (evtl. offener) Term. Die Reduktion t →1 s ist intern, d.h. ohne anteilige Normalordnungsreduktion, wenn sie nicht von einer der
Formen ist:
• R[((λx . t1 ) t2 )] →1 R0 [t01 [t02 /x]] wobei R ein Reduktionskontext ist, t1 →1
t01 , t2 →1 t02 , R →1 R0 , wobei man →1 für Kontexte wie für einen Term
mit einer extra Konstanten definiert.
• t = R[(case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) → tn+1 ; . . .})] →1
R0 [t0n+1 [t01 /x1 , . . . , t0n /xn ]] wobei R ein Reduktionskontext ist und ti →1
t0i , R →1 R0 .
• t = R[(case (λx.r) {. . . ; (lambda → t)}] →1 R0 [t0 ] wobei R ein Reduktionskontext ist und t →1 t0 , R →1 R0 .
2
Man kann die Anzahl der notwendigen Einzelreduktionen in der parallelen Reduktion r1 →1 r2 durch Markieren der Unterterme des Terms r1 mit dieser
Anzahl festhalten. Diese Anzahl entspricht der Anzahl Reduktionen, wenn man
diese “von oben“ her abarbeitet. Diese Anzahl kann man berechnen anhand der
syntaktischen Form von r1 und der Änderungen, die die 1-Reduktion bewirkt.
Sei ϕ diese Markierung, geschrieben als Abbildung von Untertermen auf die
Anzahl. Eigentlich ist das eine Abbildung der ganzen Reduktion.
• Wenn (s t) →1 s0 t0 , dann markiere (s t) mit ϕ(s) + ϕ(t)
• Wenn (λx . s) →1 (λx . s0 ), dann markiere (λx . s) mit ϕ(s).
• Wenn (case s {p1 → t1 ; . . . ; pn → tn }) →1 (case s0 {p1 → t01 ; . . . ; pn →
t0n }), dann markiere mit ϕ(s) + Σ ϕ(ti ).
• Wenn (λx . s) t →1 s0 [t0 /x], dann markiere mit 1 + ϕ(s) + ϕ(t) ∗ (#(x, s0 ))
wobei #(x, s) die Anzahl der freien Vorkommen von x in s ist.
• Wenn (case (c s1 . . . sn ) {. . . ; (c x1 . . . xn )
→
t; . . .})
→1
t[s1 /x1 , . . . , sn /xn ], dann markiere mit 1 + ϕ(t) + (Σ (ϕ(s0i ) ∗ (#(xi , t)))).
Lemma 5.3.8 Sei t ein (evtl. offener) Ausdruck. Wenn t →1 s, dann kann
no,∗
man die Reduktion zerlegen in: t −−−→ u →1 s, wobei die Reduktion u →1 s
intern ist.
no
Beweis. Dass man eine nicht-interne Reduktion t →1 s in t −→ u1 →1 s zerlegen
kann, folgt aus der Definition der Reduktion und aus der Definition von intern.
Dies kann man iterieren, bis die Reduktion u1 →1 s intern ist. Allerdings ist
nicht ganz offensichtlich, ob es terminiert. Dazu muss man die Anstrengung etwas erhöhen: Die Idee ist, dass die Anzahl ϕ der notwendigen Einzelreduktionen
vermindert wird: Es gibt zwei Fälle (Beta und Case):
16
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
no
• Wenn die erste Zerlegung so aussieht: R[(λx . s) t] −→ R[s[t/x]] →1
R0 [s0 [t0 /x]], dann ist die Zahl der Reduktionen (ϕ) in der Reduktion
R[s[t/x]] →1 R0 [s0 [t0 /x]] um mindestens 1 vermindert.
• Wenn die Zerlegung so aussieht: R[(case (c s1 . . . sn ) {. . . ; (c x1 . . . xn ) →
no
t; . . .})] −→ R[t[s1 /x1 , . . . , sn /xn ]] →1 R0 [t0 [s01 /x1 , . . . , s0n /xn ]], dann folgt
ebenfalls aus der obigen Definition, dass die Anzahl der Reduktionen mindestens um 1 vermindert ist. Die Verminderung kann größer sein, denn
eine case-Reduktion kann ganze Unterterme löschen.
Insgesamt heißt das, dass das Abspalten von no-Reduktionen terminiert.
2
no
Lemma 5.3.9 Sei t ein (evtl. offener) Ausdruck. Wenn t →1 u −→ s, dann
no,+
kann man die Reduktionen vertauschen: ∃u0 : t −−−→ u0 →1 s, so dass die
0
Reduktion u →1 s intern ist.
/u
t
no,+
no
1,int u0 _ _ _/ s
1
no,∗
no
Beweis. Die Reduktionsfolge t →1 u −→ s kann man zerlegen in: t −−−→ t0 →1
no
u −→ s, so dass t0 →1 u intern ist. Beachte, dass die 1-Relation auch trivial sein
no
kann. Es gibt nur die folgenden zwei Fälle für die Reduktion t0 →1 u −→ s:
no
1. Der erste Fall ist, dass R[(λx . t1 ) t2 ] →1 R0 [(λx . t01 ) t02 ] −→ R0 [t01 [t02 /x]],
wobei R Reduktionskontext, t1 →1 t01 , t2 →1 t02 , R →1 R0 , wobei man →1
für Kontexte wie für einen Term mit einer extra Konstanten definiert (eigentlich braucht man dafür Induktion nach der Termstruktur). In diesem
no
Fall gilt: R[(λx . t1 ) t2 ] −→ R[t1 [t2 /x]] →1 R0 [t01 [t02 /x]].
2. Der zweite Fall ist t = R[(case (c t1 . . . tn ) {. . . ; (c x1 . . . xn ) →
no
tn+1 ; . . .})] →1 R0 [(case (c t01 . . . t0n ) {. . . ; (c x1 . . . xn ) → t0n+1 ; . . .})] −→
0 0
0
0
0
R [tn+1 [t1 /x1 , . . . , tn /xn ]] wobei R Reduktionskontext, ti →1 ti , R →1 R0 .
no
In diesem Fall gilt wie im ersten t −→ R[tn+1 [t1 /x1 , . . . , tn /xn ]] →1
R0 [t0n+1 [t01 /x1 , . . . , t0n /xn ]].
no,∗
D.h. in diesem Fall ist die Reduktion vertauschbar, und wir erhalten: t −−−→
no
t1 −→ t2 →1 s. Wenn die letzte Reduktion nicht intern ist, dann kann man mit
Lemma 5.3.8 den Normalordnungsanteil abspalten.
2
no
Lemma 5.3.10 Sei s ein (evtl. offener) Ausdruck. Eine Folge s →1 s2 −→
no
. . . −→ sn wobei sn eine WHNF ist, kann man umändern in eine Reduktion, die
nur Normalordnungs-Reduktionen verwendet, um zu einer WHNF zu kommen.
no,∗
Genauer: in eine Reduktion s −−−→ s0n−1 →1 sn , bei der s0n−1 eine WHNF ist.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
17
Beweis. Das Lemma 5.3.9 kann man zum Verschieben der 1-Reduktion nach
no
no
rechts verwenden. s →1 s2 −→ . . . −→ sn wird damit im ersten Schritt zu
no,∗
no
0
s −−−→ s2 →1 s3 . . . −→ sn . Schiebt man die 1-Reduktion weiter nach rechts, so
no,∗
no,∗
erhält man schließlich: s −−−→ s02 −−−→ s3 . . . s0n−1 →1 sn . Die letzte Reduktion
ist intern, also ist s0n−1 eine WHNF.
1 /
s2
s
no,∗
no
1
s02 _ _ _/ s3
no,∗ no
1
s03 _ _ _/ s4
no
.
2
∗
Satz 5.3.11 (Standardisierung). Sei t ein (evtl. offener) Ausdruck. Wenn t −
→
t1 mit beliebigen (Beta) und (case)-Reduktionen, wobei t1 eine WHNF ist, dann
no,∗
∗
existiert eine WHNF tN F , so dass t −−−→ tN F , und tN F −
→ t1 .
t>
∗
>
no,∗
>
>

∗
t1 _ _ _ _ _ _/ t2
Beweis. Jede Reduktion ist auch eine Folge von 1-Reduktionen. Danach kann
man Lemma 5.3.10 mehrfach anwenden.
2
Satz 5.3.12 (Invarianz der Terminierung) Seien t, t0 (evtl. offene) Ausdrücke
mit t → t0 . Dann gilt t⇓ ⇔ t0 ⇓
Beweis. Das folgt aus Satz 5.3.11 (Standardisierung) und Satz 5.3.6.
2
Der nächste Satz ist der erste Satz über Korrektheit von Programmtransformationen. Er sagt aus, dass man die Reduktionsregeln Beta und Case an beliebiger
Stelle (vorwärts und rückwärts) in einem Programm verwenden kann, um das
Programm (zur Compilezeit) zur verändern, bzw. zu optimieren, ohne dass man
einen Fehler macht. Dies nennt man auch partielle Auswertung.
Satz 5.3.13 Seien t, t0 Terme, so dass t → t0 mit einer KFP-Reduktion. Dann
gilt t ∼c t0 .
18
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Beweis. Sei C irgendein Kontext. Dann gilt auch C[t] → C[t0 ], da Reduktionen
überall erlaubt sind. Wenn C[t]⇓, dann auch C[t0 ]⇓ nach Satz 5.3.12. Da das für
alle Kontexte gilt. schließen wir t ≤c t0 .
Wenn C[t0 ]⇓, dann auch C[t]⇓, ebenfalls wegen Satz 5.3.12. Deshalb gilt t0 ≤c t.
Zusammen ergibt sich t0 ∼c t.
2
Damit haben wir schon eine große Klasse von kontextuellen Gleichheiten im
Griff: alle die sich mittels Reduktion ineinander überführen lassen. Allerdings
sind das nicht alle interessanten Gleichungen, wie wir noch sehen werden.
Definition 5.3.14 Die durch Reduktion erzeugte Gleichheit ist definiert als:
∗
s ←−
−→ t gdw. es eine Folge von Reduktionen (→ und ←) zwischen s und t gibt
Diese Relation sieht Terme, die sich nur um Umbenennung von gebundenen
Variablen unterscheiden, als gleich an.
∗
∗
Es gilt, dass ←−
−→ eine Kongruenz in KFP ist. Für ←−
−→ gilt das ChurchRosser-Theorem, das man auch mit det 1-Reduktion beweisen kann:
∗
Satz 5.3.15 (Church-Rosser) Wenn s ←−
−→ t, dann existieren s0 , t0 mit
∗
∗ 0
0
0
s ≡α t und s −
→ s und t −
→ t . Als Diagramm:
s o ∗ / t
∗
∗
α
_
_
0 _
_
_
_
s
t0
∗
Satz 5.3.16 Für alle s, t gilt: s ←−
−→ t ⇒ s ∼c t
∗
In KFP ist die Umkehrung falsch: Es gibt Ausdrücke s, t mit s ∼c t, aber s ←
6 → t:
Beispiel 5.3.17 Wir geben Kombinatoren an:
Y
= \f-> (\x->
rep1 y = 1 : rep1 a
rep2 y = 1 : rep2 b
rp1
rp2
= Y (\r1
= Y (\r2
f (x x))
(\x->
f (x x))
y -> 1 : r1 a) a
y -> 1 : r2 b) b
Die Ausdrücke rp1 und rp2 sind KFP-Ausdrücke, denn die Rekursion ist direkt
kodiert.
∗
Mit Induktion kann man zeigen, dass alle Ausdrücke t1 mit t1 ←−
−→ rp1 ein a
∗
enthalten, aber kein b enthalten, während das für alle Ausdrücke t2 mit t2 ←−
−→
rp2 genau umgekehrt ist. Aus dem Satz von Church-Rosser kann man schließen,
∗
dass somit nicht gilt: rp1 ←−
−→ rp2.
Beide Ausdrücke erzeugen aber die unendliche Liste 1 : 1 : . . .. Wir werden
später sehen, dass diese Ausdrücke dann kontextuell äquivalent sein müssen.
19
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Beispiel 5.3.18 Ein weiteres Beispiel ist:
s = \x .x
t = \x -> case x of {p1 -> x; ....; p_n -> x; lambda -> x}
Das ist gerade \x -> seq x x. Diese beiden Ausdrücke können als gleich ge∗
zeigt werden, aber sind nicht gleich bzgl der Konversion ←−
−→, da nach obiger
Überlegung dann s, t auf α-äquivalente Terme reduzieren müssten, diese aber
bereits in Normalform sind.
5.3.1
Exkurs: verallgemeinerte Induktion
Eine irreflexive partielle Ordnung < auf einer Menge M ist fundiert (wellfounded), gdw. jede absteigende Kette endlich ist.
Wenn man eine fundierte Ordnung < auf einer Menge M mit Menge der kleinsten Elemente Mmin hat, dann kann man folgendes Induktionsschema verwenden, um eine Aussage P (.) für alle Elemente der Menge M zu zeigen:
((∀m ∈ Mmin : P (m))
∧ (∀x : (∀y.y < x ⇒ P (y)) ⇒ P (x)))
⇒ ∀x.P (x)
Basis
Induktionsschluss
Es gibt Standardmethoden, um aus (linearen) fundierten Ordnungen weitere
(lineare) fundierte Ordnungen zu erzeugen.
Eine fundierte Ordnung, die wir verwenden wollen, ist die lexikographische Ordnung auf Tupeln, deren Komponenten eine fundierte Ordnung haben. Es gilt:
Wenn die Ordnung < auf M fundiert ist, dann auch die lexikographische Ordnung auf den Tupeln M n , wobei (a1 , . . . , an ) < (b1 , . . . , bn )
gdw. für einen Index i : a1 = b1 , . . . , ai−1 = bi−1 , ai < bi
Die Ordnung < auf den natürlichen Zahlen ist fundiert und linear. Also ist auch
die lexikographische Ordnung auf m-Tupeln über natürlichen Zahlen für festes
m eine lineare und fundierte Ordnung. Eine nicht-lineare fundierte Ordnung ist
die echte-Teilmengen“ -Beziehung auf der Menge der endlichen Teilmengen von
”
IN.
5.4
Kontextuelle Gleichheit von Ausdrücken in
KFP
Wir betrachten und zeigen jetzt Eigenschaften der kontextuellen Approximation
und der kontextuellen Gleichheit. Nochmal zur Erinnerung die Definition:
Seien s, t KFP-Ausdrücke und C[.] ein Kontext.
s ≤c t gdw. ∀C[] : C[s]⇓ ⇒ C[t]⇓
s ∼c t gdw s ≤c t ∧ t ≤c s
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
20
Wir erinnern uns auch, dass ≤c eine Präkongruenz und ∼c eine Kongruenzrelation auf Ausdrücken ist.
Ein erstes Lemma ist:
Lemma 5.4.1
• Für verschiedene Variablen x, y gilt x 6≤c y und y 6≤c x
• Für verschiedene Konstanten c, d gilt c 6≤c d und d 6≤c c.
Beweis. Für die Variablen betrachte den Kontext C = (λy . (λx . [])) bot Nil.
Dann reduziert C[x] zu Nil, während C[y] nicht terminiert.
Für die Konstanten betrachte die Kontexte C = case [.] c -> bot;d ->
Nil; ... und C 0 =case [.] d -> bot;c -> Nil; ....
2
Man kann auch fordern, dass die Kontexte die Terme s, t schließen:
Aussage 5.4.2 In KFP gilt: s ≤c t gdw. ∀C : wenn C[s], C[t] geschlossen, dann
C[s]⇓ ⇒ C[t]⇓.
Beweis. Die eine Richtung ist klar.
Es gelte ∀C : wenn C[s], C[t] geschlossen, dann C[s]⇓ ⇒ C[t]⇓. Sei C irgendein
Kontext, so dass C[s]⇓, aber nicht C[t]⇓. Dann erweitern wir den Kontext C
so dass alle freien Variablen in C[s], C[t] eingefangen werden, indem wir für
die freien Variablen x1 , . . . xn den Kontext D[] := (λx1 , . . . , xn .C[])bot . . . bot
nehmen.
Dann gilt weiterhin D[s]⇓, denn eine WHNF ist unbeeinflusst von den Einsetzungen in die freien Variablen. Es gilt auch noch D[t]⇑, denn entweder ist die
Normalordnungsreduktion nicht terminierend, oder ergibt einen Typfehler, was
nicht durch die Einsetzung beeinflusst ist, oder bei der Reduktion von C[t] gerät
eine freie Variable in einen Reduktionskontext. Diese ist aber durch bot ersetzt,
somit gilt D[t]⇑, ein Widerspruch.
2
Wir können für Ausdrücke, die kontextuell gleich sind, mehr über die Struktur
der zugehörigen WHNF sagen.
Aussage 5.4.3 Seien s, t zwei Terme mit s ≤c t. Dann gilt einer der drei Fälle:
1. s⇑.
∗
∗
2. s −
→ (c s1 . . . sn ) und t −
→ (c t1 . . . tn ) für einen Konstruktor c und si ≤c ti
für alle i.
∗
∗
3. s −
→ λx.s0 und t −
→ λx.t0 und für alle r: ((λx.s0 ) r) ≤c ((λx.t0 ) r).
Beweis. Wir nehmen an, dass s⇓, so dass wir nur zeigen müssen, dass dann der
zweite oder dritte Fall eintritt.
Mit Kontexten der Form (case [] {alts}) ist sofort zu zeigen, dass aus s ≤c t
21
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
folgt, dass die WHNF für t den gleichen Topkonstruktor wie s haben muss, bzw.
sowohl s als auch t Abstraktionen sind.
∗
∗
Angenommen, s −
→ (c s1 . . . sn ). Dann gilt t −
→ (c t1 . . . tn ). Um für
ein i die Beziehung si ≤c t zu zeigen, nehme den Kontext Ci =
case [] {... ; (c x_1 ... x_i ... x_k) -> x_i; ... } . Da aus s ≤c
t auch C[s] ≤c C[t] folgt, und man auch reduzieren darf wegen Satz 5.3.16, folgt
sofort: si ≤c ti .
∗
∗
Angenommen, s −
→ λx.s0 . Dann gilt t −
→ λx.t0 . Um ((λx.s0 ) r) ≤c ((λx.t0 ) r) für
alle r zu zeigen, nehme den Kontext Cr := ([] r).
2
Aussage 5.4.4 Seien s, t zwei Terme mit s ∼c t.
Dann gilt einer der drei Fälle:
1. s⇑ und t⇑.
∗
∗
2. s −
→ (c s1 . . . sn ) und t −
→ (c t1 . . . tn ) für einen Konstruktor c und si ∼c ti
für alle i.
∗
∗
3. s −
→ λx.s0 und t −
→ λx.t0 und für alle r: ((λx.s0 ) r) ∼c ((λx.t0 ) r).
Definition 5.4.5 Ein Konstruktorterm ist ein Ausdruck, der nur aus Konstruktoren besteht.
Beachte, dass damit endliche Konstruktorterme“
”
drücke sind immer endlich.
gemeint sind, denn Aus-
Satz 5.4.6 Erhält man für einen Ausdruck mittels zweier verschiedener Auswertungen jeweils Konstruktorterme, so sind diese gleich.
∗
∗
Formal: s −
→ s1 , s −
→ s2 wobei s1 , s2 Konstruktorterme sind, dann gilt s1 = s2 .
Beweis. Folgt aus der Aussage 5.4.4 mit Induktion über die Tiefe des Konstruktorausdrucks.
2
Jetzt können wir auch noch zeigen, dass man für Ausdrücke mit freien Variablen
eingeschränktere Kriterien für die kontextuelle Äquivalenz hat:
Lemma 5.4.7 Seien s, t KFP-Ausdrücke mit den freien Variablen x1 , . . . , xn
und sei ρ eine Umbenennung der Variablen xi . D.h. eine Injektion mit ρ(x) = yi .
Dann gilt s ≤c t gdw. sρ ≤c tρ.
Aussage 5.4.8 Seien s, t KFP-Ausdrücke mit den freien Variablen x1 , . . . , xn .
Dann gilt s ≤c t gdw. λx1 , . . . , xn .s ≤c λx1 , . . . , xn .t.
Beweis. Die eine Richtung ist klar.
Sei λx1 , . . . , xn s ≤c λx1 , . . . , xn t, und sei C ein Kontext, so dass C[s], C[t] geschlossen sind, und sei C[s]⇓.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
22
Bilde den Kontext C 0 := C[(λx1 , . . . , xn .[])x1 . . . xn ). Dann reduziert C 0 [s] nach
einigen Beta-Reduktionsschritten zu C[s], also gilt C 0 [s]⇓ nach Satz 5.3.16. Da
C 0 [s] = C[(λx1 , . . . , xn .s)x1 . . . xn ], erhält man wegen der Voraussetzung auch
C[(λx1 , . . . , xn .t)x1 . . . xn ]⇓. Da dies zu C[t] reduziert, erhält man C[t]⇓, und
damit gilt die Behauptung.
2
5.4.1
Simulation und Bisimulation
Zum Erkennen der kontextuellen Gleichheit von zwei Ausdrücken ist es hilfreich, wenn man diese nicht in allen (unendlich) vielen Kontexten ausprobieren
muss, sondern wenn es ausreicht, die Terme selbst (d.h. im leeren Kontext) zu
reduzieren, und dies evtl. zu iterieren; d.h. die Komponenten des Ergebnisses zu
reduzieren oder falls es eine Funktion ist, diese auf ein Argument anzuwenden.
Dies leistet die sogenannte Simulation bzw. Bisimulation.
Definition 5.4.9 Wir definieren ≤b als den größten Fixpunkt des folgenden
Operators [.] auf Relationen R auf geschlossenen Termen.
Sei R eine Relation. Die Relation s [R] t für geschlossene Terme s, t gilt, wenn
folgendes erfüllt ist:
Wenn s⇓(c s1 . . . sn ), dann t⇓(c t1 . . . tn ) und si R ti für alle i.
und
wenn s⇓(λx.s0 ), dann t⇓(λx.t0 ) und für alle geschlossenen r:
((λx.s0 ) r) R ((λx.t0 ) r)
Man definiert s ∼b t, gdw s ≤b t und t ≤b s.
Die Relation ≤b nennt man auch Simulation und ∼b Bisimulation.
Die Relation s ∼b t gilt, wenn man s, t mittels WHNF-Reduktion und dann
entweder durch Anwenden auf gleiche Argumente, oder Betrachten von Untertermen nicht unterscheiden kann. Diese Betrachtungsweise wird durch die
Definition mittels größtem Fixpunkt erreicht.
Einschub Knaster-Tarski Fixpunktsatz
Ein vollständiger Verband A ist ein Verband, d.h. eine Menge mit einer partiellen
Ordnung ≤, so dass glb und lub von zwei Elementen existieren. Vollständig
bedeutet, dass man zu jeder Teilmenge S ⊆ A den glb und lub bilden kann.
Ein wichtiges Beispiel zu vollständigen Verbänden ist der VerbandSder Teilmengen einer
T Menge M geordnet mit ⊆. Hier kann man lub(S) = (S) und
glb(S) := S definieren.
Wir nennen a ist ein Fixpunkt einer Funktion f , gdw. f (a) = a;
a ist ein Post-Fixpunkt einer Funktion f , gdw. a ≤ f (a).
Entsprechend ist a ist ein Pre-Fixpunkt einer Funktion f , gdw. f (a) ≤ a.
Der Fixpunktsatz von Knaster-Tarski:
Sei A ein vollständiger Verband und f einen monotone Funktion. Dann existiert
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
23
ein Fixpunkt von f .
Der größte Fixpunkt von f ist lub{x ∈ A | x ≤ f (x)}.
Der kleinste Fixpunkt von f ist glb{x ∈ A | f (x) ≤ x}.
Da man die Relationen auf den Ausdrücken als Mengen von Paaren ansehen
kann, hat man einen vollständigen Verband von Relationen, und man kann
den Fixpunktsatz von Knaster-Tarski verwenden. Wir formulieren den Satz auf
Mengen mit Vereinigung und Untermenge:
Satz 5.4.10 Sei (L, ⊆) ein (vollständiger) Mengen-Verband und sei f : L → L
eine monotone Funktion. Dann ist
[
m = νf = {R ∈ L | R ⊆ f (R)}
ein größter Fixpunkt, und auch ein größter Post-Fixpunkt.
\
k = µf = {R ∈ L | f (R) ⊆ R}
ein kleinster Fixpunkt, und auch ein kleinster Pre-Fixpunkt.
Die Funktion [·] ist monoton auf Relationen, denn bei einer kleineren Relation
R stehen auch weniger Paare von Ausdrücken in der [R]-Relation. Deshalb kann
man den Satz von Knaster-Tarski auf Relationen und die Funktion [·] anwenden.
(Satz von Kleene)
Wenn die Funktion f für die man den Fixpunkt bilden will, die Eigenschaft der
(glb)-Stetigkeit hat:
D.h. für alle absteigenden Folgen s1 ≥c s2 ≥c . . ., die einen glb s haben, gilt:
glb(f (si )) = f (glb(si )).
Dann kann man den größten Fixpunkt von f auch definieren über eine Folge
von Relationen ≤b,i mit folgender Eigenschaft:
Entsprechendes gilt für lub-stetige Funktionenund den kleinsten Fixpunkt.
Beim oben in Def. 5.4.9 definierten Operator [·] auf Relationen kann man die
Eigenschaft der Stetigkeit nicht nachweisen, man kann eine entsprechende Folge
und deren glb aber als eine Näherung verwenden:
s ≤b,0 t für alle s, t.
s ≤b,i+1 t wenn folgendes gilt:
Wenn s⇓(c s1 . . . sn ), dann t⇓(c t1 . . . tn ) und sj ≤b,i tj für alle
j = 1, . . . , n;
und
wenn
s⇓(λx.s0 ),
dann
t⇓(λx.t0 )
und
für
alle
r:
((λx.s0 ) r) ≤b,i ((λx.t0 ) r)
Die Relation s ≤b t ist dann enthalten in der Relation: ∀i : s ≤b,i t.
24
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Man braucht noch die offene Erweiterung ≤ob : von ≤b :
s ≤ob t gdw. für alle Einsetzungen σ von geschlossenen Termen: σ(s) ≤b σ(t)
gilt. Entsprechend ist ∼ob definiert.
Es gilt folgende Aussage zum Zusammenhang zwischen ≤b und ≤c :
Satz 5.4.11 (siehe Howe-1989, 1996). Es gelten die Aussagen:
≤ob = ≤c
∼ob = ∼c
Es folgt sofort, dass ⊥ ≤c t ist für alle t, denn für alle geschlossenen t: ⊥ ≤b t
ist offensichtlich. Für offene t folgt es dann auch aus der Definition von ≤ob .
Leider können wir das Theorem 5.4.11 nicht im Rahmen des Skriptes zeigen,
da man dafür spezifische Methoden verwenden muss. Was man allerdings mit
unseren Mitteln zeigen kann, ist eine Richtung:
Aussage 5.4.12 Es gilt ≤c
⊆
≤ob
Beweis. Es genügt zu zeigen, dass ≤c,0 (≤c,0 ist ≤c eingeschränkt auf geschlossene Terme) ein Post-Fixpunkt des Operators [.] ist, d.h. dass ≤c,0 ⊆ [≤c,0 ],
denn nach dem Satz von Knaster-Tarski ist ≤b ein größter Post-Fixpunkt von
[·]. Das ist aber gerade der Inhalt von Aussage 5.4.3, denn aus s ≤c,0 t folgt
einer der drei Fälle in 5.4.3, und daraus folgt, dass s [≤c,0 ] t. Für offene Terme
folgt es dann aus der offenen Erweiterung.
2
Der Unterscheidung offen / geschlossen bietet auch keine Überraschungen:
Aussage 5.4.13 Seien s, t KFP-Ausdrücke mit den freien Variablen
x1 , . . . , xn . Dann gilt s ≤c t gdw. für alle Einsetzungen σ von geschlossenen Termen für freie Variablen σ(s) ≤c σ(t).
Beweis. Die eine Richtung folgt leicht aus der Aussage 5.4.8 oben.
Die andere Richtung folgt aus dem Satz von Howe: ≤c,0
⊆
≤ob und da
≤b = ≤c,0 ist, wobei ≤c,0 die Einschränkung von ≤c auf geschlossene Terme
ist.
2
Durch die Definition von ≤b als größter Fixpunkt (und größter Postfix-Punkt)
und durch die Gleichheit von ≤b und ≤c gilt folgendes Induktionsprinzip (CoInduktion).
Lemma 5.4.14 Wenn für eine Relation R gilt: R
R ⊆ ≤b = ≤c .
⊆
[R], dann gilt
Das einfache Prinzip des Kleene-Satzes kann man leicht durch die abstraktere
Methode der Co-Induktion ersetzen, um konkrete Gleichheiten zu zeigen:
Lemma 5.4.15 Gegeben zwei Terme s, t. Sei R1 so definiert, dass s0 R1 t0 gdw.
(s0 , t0 ) Grundterme sind, die eine gemeinsame Einsetzung von Termen für die
Variablen in (s, t) ist. Wenn
R1 ⊆ [R1 ∪ ≤b ], dann gilt R1 ⊆ ≤b , und somit s ≤b t, also auch s ≤c t,
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
25
Beweis. Aus R1 ⊆ [R1 ∪ ≤b ] folgt (R1 ∪ ≤b ) ⊆ [R1 ∪ ≤b ]. Damit gilt die
Behauptung, da ≤c und ≤b übereinstimmen, und da ≤b der größte Fixpunkt
von [·] ist, und somit R1 ∪ ≤b ⊆ ≤b , also R1 ⊆ ≤b = ≤c . Die Relation
s ≤c t folgt aus der Aussage 5.4.13.
2
Aus dem bisherigen folgt auch:
Lemma 5.4.16 Gegeben zwei Terme s, t. Sei R1 so definiert, dass s0 R1 t0 gdw.
(s0 , t0 ) Grundterme sind, die eine gemeinsame Einsetzung von Termen für die
Variablen in (s, t) ist. Wenn
R1 ⊆ [R1 ∪ ∼b ], dann gilt R1 ⊆ ∼b , also auch R1 ⊆ ∼c und s ∼c t.
Diese Aussagen helfen, die Gleichheit von Ausdrücken zu prüfen:
Beispiel 5.4.17 s = λx, y.if x then (Cons x y) else (Cons True y)
t = λx, y.if x then (Cons True y) else (Cons True y)
Diese Terme haben keine freien Variablen. Die Relation R1 besteht genau aus
(s, t). Man muss also nur zeigen, dass (s, t) ∈ [{(s, t)}∪ ∼b ]. Dazu muss man
s [{(s, t)}∪ ∼b ] t testen. das gilt, wenn man (nach zwei Schritten) für alle
(geschlossenen) Ausdrücke r1 , r2 : die Gleichheit von s r1 r2 und t r1 r2 testet,
d.h. {(s, t)}∪ ∼b
Für r1 = True bzw. r1 = False ergibt sich der gleiche Term. Aber für r1 = bot
ergibt sich ebenfalls das gleiche: s bot r2 = bot und t bot r2 = bot.
Beachte aber, dass die Terme s, t nicht kontextuell äquivalent zu
λx, y.(Cons True y) sind.
Beispiel 5.4.18 Im folgenden Beispiel gehen wir davon aus, dass repeat und
map in KFP geeignet definiert sind.
Wenn s = (repeat 1) und t = map (λx.1) (repeat 2), gilt dann s ∼c t?
Hierzu kann man s und t reduzieren und erhält jeweils eine WHNF:
1 : (repeat 1) und 1 : map (λx.1) (repeat 2). Die rekursive Frage ist dann wieder die gleiche, nämlich ob s ∼c t. Hier kann man das Lemma 5.4.16 anwenden.
Damit folgt s ∼c t.
Diese Methode werden wir noch erweitern in einem der folgenden Kapitel.
5.4.2
Das Kontextlemma
Ein wichtiges Lemma zur kontextuellen Approximation ist die Einschränkung
des Kriteriums “für alle Kontexte“ auf Reduktionskontexte. Das kann in anderen Kalkülen (i.a. call-by-need-Kalküle) als gleichwertiger oder überlegener Weg
zu den Aussagen wie Korrektheit von Programmtransformationen und Standardisierungslemma verwendet werden. Wir erwähnen das Kontextlemma wegen
seiner herausragenden Rolle in anderen Reduktionskalkülen; es lässt sich auch
in KFP unabhängig nachweisen, allerdings scheint es nicht der richtige Weg
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
26
zu sein, die Korrektheit der KFP-Kalkülregeln als Programmtransformationen
nachzuweisen. Mögliche Gründe sind: KFP-Reduktionskontexte können offene
Terme nicht abschließen, was in anderen Kalkülen (mit let) möglich ist; In KFP
wird call-by-name verwendet (d.h. die Beta-Reduktion setzt beliebige Ausdrücke
ein). In call-by-need Kalkülen
Für das Kontextlemma benötigen wir den technischen Begriff des Multikontexts.
Das ist eine Verallgemeinerung des Begriffs Kontext auf mehrere Löcher. Dazu
kann man z.B. neue Konstanten nehmen, bezeichnet als ·1 , . . . , ·n und Terme
bauen, die diese Konstanten enthalten. Wir nehmen zusätzlich an, dass diese
Konstanten genau einmal vorkommen, wenn sie erwähnt sind, d.h. C[·1 , . . . , ·n ]
ist ein Ausdruck, der jede Konstante ·i genau einmal enthält.
Lemma 5.4.19 (Kontext-Lemma)
Seien s, t geschlossene Ausdrücke.
Wenn für alle (geschlossenen) Reduktionskontexte R: R[s]⇓ ⇒ R[t]⇓ gilt, dann
gilt auch s ≤c t.
Beweis. Der hier gegebene Beweis ist unabhängig von den Beweisen mittels
der 1-Relation, und auch unabhängig von den Bsimulationssätzen. Beachte im
folgenden, dass R[s], R[t] geschlossene Terme sind.
Wir beweisen die allgemeinere Behauptung:
Wenn für die geschlossenen Terme si , ti : ∀i, R : R[si ]⇓ ⇒
R[ti ]⇓ erfüllen, dann gilt für alle Multikontexte C[·1 , . . . , ·m ]:
Wenn C[s1 , . . . , sn ], C[t1 , . . . , tn ] geschlossen sind, dann gilt
C[s1 , . . . , sn ]⇓ ⇒ C[t1 , . . . , tn ]⇓.
Wie nehmen an, dies sei falsch, Dann gibt es ein Gegenbeispiel, d.h. geschlossene Terme si , ti mit ∀i, R : R[si ]⇓ ⇒ R[ti ]⇓, aber es gibt einen Multikontext
C[·1 , . . . , ·n ], so dass C[s1 , . . . , sn ]⇓, aber C[t1 , . . . , tn ]⇑. Wir wählen den Multikontext so, dass er minimal ist bzgl. der lexikographischen Ordnung mit den
Komponenten
1. Die Anzahl der Normalordnungsreduktionen von C[s1 , . . . , sn ],
2. Die Anzahl der Löcher von C.
Wenn ein Loch von C[·1 , . . . , ·n ] in einem Reduktionskontext ist, d.h., einer der
Kontexte C[·, t2 , . . . , tn ], C[t1 , ·, t3 , . . . , tn ], . . . , C[t1 , . . . , tn−1 , ·] ist ein Reduktionskontext, dann nehmen wir der Einfachheit halber an, das erste Loch sei
in einem Reduktionskontext. Dann ist C[·, t2 , . . . , tn ] ein Reduktionskontext.
Sei C 0 := C[s1 , ·2 , . . . , ·n ]. Da C 0 [s2 , . . . , sn ] ≡ C[s1 , . . . , sn ], haben beide dieselbe Normalordnungsreduktion. Die Anzahl der Löcher von C 0 ist kleiner als
die von C, also gilt C 0 [t2 , . . . , tn ]⇓. Das bedeutet, dass C[s1 , t2 , . . . , tn ]⇓. Da
C[·, t2 , . . . , tn ] ein Reduktionskontext ist, folgt aus den Bedingungen des Lemmas : C[t1 , t2 , . . . , tn ]⇓, was ein Widerspruch ist.
Wenn keiner der Kontexte C[·, t2 , . . . , tn ], C[t1 , ·, t3 , . . . , tn ], . . . ,
C[t1 , . . . , tn−1 , ·] ein Reduktionskontext ist, dann ist entweder C[s1 , . . . , sn ]
eine WHNF, oder die zwei Terme C[s1 , . . . , sn ] und C[t1 , . . . , tn ] werden mit
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
27
derselben Normalordnungsreduktion reduziert. Das liegt daran, dass das Finden
des Normalordnungsredex von oben nach unten geschieht und in diesem Falle
unabhängig von den Ausdrücken in den Löchern ist.
Falls C[s1 , . . . , sn ] eine WHNF ist, dann terminiert die Suche nach einem Normalordnungsredex innerhalb des Kontexts C, somit ist auch C[t1 , . . . , tn ] eine
WHNF.
Wir betrachten den Fall dass C[s1 , . . . , sn ] keine WHNF ist, aber C[s1 , . . . , sn ]⇓.
Die Normalordnungsreduktion produziert aus C[s1 , . . . , sn ] einen Ausdruck
C 0 [u1 , . . . , um ] wobei ui = sφ(i) für eine Abbildung φ : [1..m] → [1..n].
Es ist möglich dass m > n. Die Anzahl der Normalordnungsschritte von
C 0 [u1 , . . . , um ] ist um 1 kleiner als die von C[s1 , . . . , sn ]. Jetzt zeigt die Annahme der Minimalität, dass dann auch C 0 [v1 , . . . , vm ]⇓ mit vi = tφ(i) . Da der
Ausdruck C[t1 , t2 , . . . , tn ] in Normalordnung auf C 0 [v1 , . . . , vm ] reduziert, gilt
auch C[t1 , t2 , . . . , tn ]⇓.
2
Beispiel 5.4.20 Dieses Beispiel zeigt, dass der Beweis des Kontextlemma zu
KFP nicht auf offene Terme verallgemeinerbar ist: Nehme den Multikontext
C = (λx.·1 )·2 . Dann reduziert C[[x], 1] zu [1], aber weder ist eines der Löcher
in einem Reduktionskontext, noch kann man alle eingesetzten Ausdrücke unverändert im Redukt wiederfinden, z.B. ist [x] zu [1] geworden. D.h. der Beweisschritt, der Normalordnungsreduktion anwendet, und dann das Resultat als
neuen Multikontext mit eingesetzten Termen beschreibt, ist in diesem Fall nicht
mehr zu begründen.
Leider gibt das Kontext-Lemma keine Information über die kontextuelle Äquivalenz von offenen Ausdrücken. In Zusammenarbeit mit folgender Aussage kann
man das Kontextlemma auch in allgemeineren Zusammenhängen verwenden.
Wir beweisen diese Aussage, die oben aus dem Howe-Satz folgt, nochmal direkt.
Aussage 5.4.21 Seien s, t Ausdrücke mit den freien Variablen x1 , . . . , xn .
Dann:
s ≤c t gdw. für alle geschlossenen ti gilt: s[t1 /x1 , . . . , tn /xn ] ≤c
t[t1 /x1 , . . . , tn /xn ]
Beweis. Die nichttriviale Richtung der Implikation ist: Wenn für alle geschlossenen ti gilt: s[t1 /x1 , . . . , tn /xn ] ≤c t[t1 /x1 , . . . , tn /xn ], dann gilt s ≤c t.
Induktion nach der Anzahl der freien Variablen in s, t.
Es genügt, für s0 := λx1 , . . . xn .s und t0 := λx1 , . . . xn .t zu zeigen, dass s0 ≤c t0 .
Jetzt kann man das Kontextlemma anwenden.
Sei R ein Reduktionskontext. Dann ist das Loch entweder in einem case.
Dann sind die Reduktionen von R[s0 ], R[t0 ] beide mit Typfehler, oder das Loch
wird angewendet, d.h. R[] = R0 ([] r), wobei r geschlossen ist. Dann gilt wegen der Induktionsannahme λx2 , . . . , xn .s[r/x1 ] ≤c λx2 , . . . , xn .t[r/x1 ]. D.h.
R[s0 ] ∼c R0 ([s0 ] r) ∼c R0 (λx2 , . . . , xn .s[r/x1 ]). Somit gilt: R[s0 ]⇓ ⇒ R[t0 ]⇓. Das
Kontextlemma zeigt jetzt s0 ≤c t0 , und somit auch s ≤c t.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.4.3
28
Programmtransformationen
Programmtransformationen sind zunächst mal alle Veränderungen eines Programms P resultierend in einem Programm P 0 . Natürlich soll diese Transformation nicht das Programm verfälschen, z.B. aus einem Programm, das Zahlen
testet, ob sie gerade sind, ein Programm machen, das Zahlen testet, ob sie ungerade sind. Dies wird durch den Begriff der kontextuellen Gleichheit erfasst.
Definition 5.4.22 Eine Programmtransformation ist eine Relation T auf Ausdrücken. Man sagt zu e1 T e2 , dass T den Ausdruck e1 zu einem Ausdruck e2
transformiert.
Die Programmtransformation T ist korrekt, wenn aus e1 T e2 stets e1 ∼c e2
folgt.
Die Programmtransformation T ist modular, wenn für jeden Kontext C aus
e1 T e2 auch C[e1 ] T C[e2 ] folgt.
Modularität einer Programmtransformation bedeutet, dass man sie auf Unterterme anwenden kann ohne den Rest des Programms zu kennen.
Beachte, dass die Korrektheit von Programmtransformationen zunächst nur bedeutet, das diese während des Kompilierens verwendet werden dürfen. Während
des Programmlaufs, d.h. in eine Normalordnungsreduktion eingeschoben darf eine Transformation nur verwendet werden, wenn man darauf achtet, dass sich
die Terminierungseigenschaften dadurch nicht ändern. Eine veränderte Normalordnung könnte auch eine gewünschte Verbesserung der Auswertung sein, aber
dazu sind andere Betrachtungen notwendig: Man muss dazu verschiedene Reduktionsstrategien vergleichen, was wir an anderer Stelle machen werden.
Beispiel 5.4.23 Ein triviales Beispiel für eine veränderte Normalordnungsreduktion, die aus korrekten Programmtransformation während der Normalordnungsreduktion besteht, aber ein falsches Terminierungsverhalten hat, ist:
Mache einen Normalordnungsreduktionsschritt, und dann mache diesen wieder
rückgängig. Diese “Reduktion“ ändert die Terme nicht, kann also i.a. nicht terminieren.
Korollar 5.4.24 Aus dem Satz 5.3.16 oben folgt, dass die Beta- und die CaseReduktion an beliebigen Stellen eines Programms während des Kompilierens als
Programmtransformationen verwendet werden dürfen. Das gilt auch für die umgekehrten Regeln. D.h. Beta-Expansion und Case-Expansion.
Die meisten KFP-Programmtransformationen sind eine Kombination von
Kalkülregeln. Es gibt auch solche, die keine Kombination von Kalkülregeln sind.
(z.B. Assoziativität von append (++))
Die sogenannte Lambda-Lifting-Transformation ist im ungetypten LambdaKalkül, d.h. ohne case und Konstruktoren, eine bekannte Methode, um Superkombinatorprogramme herzustellen.
Definition 5.4.25 Sei λx.e ein Ausdruck, so dass e eine freie Variable y
enthält. Dann ist die Lambda-Lifting Transformation:
ll
λx.e −
→ (λz.λx.e[z/y]) y, wobei z eine neue Variable ist.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
29
Aussage 5.4.26 Lambda-Lifting ist in KFP eine korrekte Programmtransformation.
Beweis. Das folgt daraus, dass sich (λz.λx.e[z/y]) y mittels Beta-Reduktion
wieder zu λx.(e[z/y])[y/z] = λx.e reduzieren läßt, und dass Beta eine korrekte
Programmtransformation ist.
2
Die sogenannte η-Transformation ist ebenfalls im ungetypten Lambda-kalkül
bekannt. Man kann diese zum Lambda-Kalkül hinzunehmen oder nicht, was
natürlich verschiedene Reduktions-Gleichheiten ergibt.
Definition 5.4.27 Sei e ein Ausdruck, der die Variable x nicht frei enthält.
Dann ist die η-Transformation definiert als:
η
→ e
λx.(e x) −
Lemma 5.4.28 Die η-Transformation ist i.a. nicht korrekt in KFP.
Beweis Dazu genügt es einen Ausdruck anzugeben, für den das falsch ist.
Der Ausdruck (λx.bot x) hat im leeren Kontext eine WHNF, aber es gilt
η
λx.bot x −
→ bot, und bot hat keine WHNF.
2
Die folgende Variante der η-Reduktion ist korrekt:
Definition 5.4.29 Sei e ein Ausdruck, der die Variable x nicht frei enthält,
und der eine WHNF hat, die eine Abstraktion ist (bzw. von Typ“a → b ist).
”
Dann ist die η 0 -Transformation definiert als:
η0
λx.(e x) −→ e
Aussage 5.4.30 Die η 0 -Transformation ist korrekt.
Beweis. Hierzu genügt es, den Satz zur Bisimulation zur verwenden: Da beide
eine WHNF haben, genügt es, beide Ausdrücke auf allen geschlossenen Argumenten r zu testen: ((λx.(e x)) r) reduziert zu (e r).
2
Man kann diese Transformation noch etwas allgemeiner fassen: Die ηTransformation ist korrekt, wenn man sie im richtigen Kontext verwendet:
η 00
((λx.(e x)) t) −−→ (e t) . Diese Transformation ist ebenfalls mittels des
Bisimulationssatzes leicht als korrekt zu erkennen.
5.4.4
Kleinste und Größte Elemente bzgl. ≤c
Wir betrachten kleinste und größte Elemente bzgl. ≤c , wobei wir das modulo
∼c meinen. Was wir im folgenden zeigen können, ist die kontextuelle Äquivalenz
aller nichtterminierenden Ausdrücke und aller ungetypten Ausdrücke, insbesondere ⊥.
Offene Ausdrücke, die ungetypt sind, oder die eine nichtterminierende Normalordnungsreduktion haben, behalten diese Eigenschaft auch nach Einsetzung von
Termen für die freien Variablen:
30
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
n
1. Sei t −
→ t0 , so dass t0 direkt ungetypt ist. (wir haben das dynamisch
ungetypt genannt). Dann ist für jede Einsetzung ρ auch der Ausdruck t ρ
dynamisch ungetypt.
Das liegt daran, dass die Normalordnungsreduktion freie Variablen nicht
unmittelbar anfasst, sondern nur mitschleppt.
∞,n
∞,n
2. Sei t −−−→. Dann gilt für jede Einsetzung ρ auch t ρ −−−→. Auch hier ist
die Normalordnungsreduktion die gleiche.
Wir zeigen mit den Mitteln des Skriptes (nicht über den Bisimulationssatz),
dass ⊥ das kleinste Element ist (modulo ∼c ):
Lemma 5.4.31 Sei s ein geschlossener Ausdruck mit s⇑ und t ein weiterer
geschlossener Ausdruck. Dann gilt s ≤c t.
Beweis. Sei R ein Reduktionskontext. Jede Normalordnungsreduktion von R[s]
muss zuerst s in WHNF reduzieren, was nach Voraussetzung nicht geht. Damit
gilt R[s]⇓ ⇒ R[t]⇓ und somit folgt aus dem Kontextlemma, dass s ≤c t
2
Vorsicht bei offenen Termen! z.B. gilt x⇑, aber x 6∼c ⊥.
Lemma 5.4.32 Sei s ein (evtl. offener) Ausdruck, so dass für alle Einsetzungen ρ: (s ρ)⇑, und sei t ein weiterer Ausdruck. Dann gilt s ≤c t.
Beweis. Das Lemma oben zeigt, dass für alle Einsetzungen ρ, die alle freien
Variablen von s, t mit geschlossenen Ausdrücken ersetzen, dass sρ⇑, und damit
sρ ≤c tρ. Lemma 5.4.21 zeigt dann auch s ≤c t.
2
Damit gibt es ein kleinstes Element in der ≤c -Ordnung in der Menge der geschlossenen Ausdrücke (modulo ∼c ), nämlich bot, auch als ⊥ geschrieben.
Aussage 5.4.33 Seien s, t zwei (evtl. offene) Ausdrücke, die entweder eine
nicht terminierende Normalordnungsreduktion haben oder direkt nicht wohlgetypt sind. Dann gilt s ∼c t.
Beweis. Folgt aus obigem Lemma, da wegen s⇑ und t⇑ sowohl s ≤c t als auch
t ≤c s gilt.
2
Beachte, dass es auch offene Terme gibt, die eine unendliche Normalordnungsreduktion haben: Z.B. bot x. Für diese gilt ebenfalls bot x ∼c bot.
Aussage 5.4.34 Wenn es mindestens einen Konstruktor mit Stelligkeit ≥ 1
gibt, dann gibt es kein größtes Element in der ≤c -Ordnung
Beweis. Sei c ein Konstruktor der Stelligkeit n: Ein größtes Element s muss eine
WHNF haben. Dafür gibt es zwei Möglichkeiten:
• s = λx . s0 . In dem Fall ist c ⊥
. . ⊥} 6≤c s: Nehme den Kontext
| .{z
n
case [] {(c x1 . . . xn ) → c; lambda → ⊥; . . .}.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
31
• s = d s1 . . . sm wobei d ein Konstruktor der Stelligkeit m ist. In dem Fall
ist λx.x 6≤c s: Nehme den Kontext case [] {(d x1 . . . xn ) → bot; lambda →
(d ⊥ . . . ⊥); . . .}.
2
Diese Aussage zeigt, dass eine Einbettung (d.h. eine Implementierung, die die
kontextuelle Ordnung erhält) von KFP in den einfachen ungetypten LambdaKalkül nicht 1-1 sein kann, denn der Lambda-Kalkül ohne Konstruktoren mit
der kontextuellen Ordnung wie in “The lazy Lambda-Calculus“ von Abramsky
beschrieben, hat ein eindeutiges größtes Element (modulo der kontextuellen
Ordnung): (Y K)1 , das ist ein Lambda-Ausdruck, der beliebig viele Argumente
schluckt, ohne sich bzgl. ∼c zu verändern: (Y K) t ∼c (Y K), wobei ∼c hier die
kontextuelle Ordnung im lazy Lambda-Kalkül ist. Der Ausdruck Y K entspricht
dem Superkombinator f x = f .
5.4.5
Auswertungsreihenfolgen
Aus dem Satz über die Korrektheit der Anwendbarkeit von Reduktionen an
beliebigen Stellen in einem Ausdruck folgt, dass man Ausdrücke in einem Programm in beliebiger Reihenfolge auswerten darf, solange man die Reduktionsregeln verwendet. Allerdings gilt das nur, wenn die Auswertungen auch terminieren für Ausdrücke mit WHNF.
Definition 5.4.35 Eine Auswertungsstrategie S ist eine Relation t →S t0 auf
Termen, wobei zumindest t →S t0 ⇒ t ∼c t0 gelten soll. Diese Relation muss
effektiv sein, d.h es muss einen Algorithmus geben, der bei Eingabe des Terms
t das Redukt t0 berechnet und ausgibt, oder sagt: keine Reduktion möglich, oder:
ist eine S-Normalform. Die transitive, reflexive Hülle der Reduktion bezeichnen
∗
∗
wir mit −
→S . Falls t −
→S t0 , wobei t0 eine S-Normalform, dann schreiben wir
t⇓S . Wir nehmen an, dass S-Normalformen auch WHNFs sind.
• Seien S1 , S2 zwei Strategien. Die Strategie S1 ist strikter als S2 , gdw. für
alle Terme t: t⇓S1 ⇒ t⇓S2 ,
• Eine Strategie S ist korrekt, gdw. für alle Terme t: t⇓S ⇒ t⇓. D.h., wenn
sie strikter als die Normalordnungsreduction ist.
Aus unseren Sätzen folgt sofort, dass alle Strategien korrekt sind.
Folgende Normalform für strikte Auswertung entspricht der Auswertung der
Argumente einer Anwendung vor dem Einsetzen der Argument.
Definition 5.4.36 Eine strikte Normalform ist definiert als:
• Eine Abstraktion ist eine strikte Normalform.
• Wenn s1 , . . . , sn strikte Normalformen sind, dann auch c s1 . . . sn für
einen Konstruktor c mit ar(c) ≥ n.
1 Y ist der Fixpunktkombinator wie in Abschnitt 5.6.2 beschrieben und K ist definiert als
K xy=x
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
32
Beachte, dass in dieser Definition der kleinste Fixpunkt gebildet wird.
Zwei Strategien bzw. Methoden der Reduktion haben eigene Namen:
Normalordnung (normale Reihenfolge, normal-order, nicht-strikte Auswertung), die wir bereits kennen.
Anwendungsordnung (applikative Reihenfolge, applikative Ordnung, strikte Auswertung). Hier wird verlangt, dass man die Beta-Reduktion nur
verwenden darf, wenn das Argument ausgewertet ist, d.h., eine strikte
Normalform ist. Ebenso, dass der zu analysierende Ausdruck im case eine strikte Normalform ist. Die zur Strategie gehörige Normalform ist die
strikte Normalform.
Die Anwendungsordnung ist die Strategie, die in Lisp, und ML verwendet wird.
Es gibt auch andere Varianten der Auswertung, in denen Normalform durch
etwas anderes (z.B. HNF) ersetzt ist.
Aus Satz 5.4.6 folgt, dass die verschiedenen Methoden die gleichen Resultate
liefern, wenn die Auswertung mit Konstruktortermen terminiert.
Allerdings kann das Verhalten bzgl. Terminierung verschieden sein:
Beispiel 5.4.37 Sei K = λx.(λy . x).
Dann wertet die Anwendungsordnung den Ausdruck K Nil bot so aus, dass
zuerst versucht wird, bot in Normalform zu bringen. Das gelingt nicht, also
terminiert das nicht.
Die Normalordnung wertet den Ausdruck K Nil bot so aus, dass zuerst der Ausdruck ((λy . Nil) bot) entsteht. Danach wird bot eingesetzt, und der Ausdruck
reduziert zu Nil. D.h. in diesem Fall terminiert die Auswertung.
Beispiel 5.4.38 Ein weiteres Beispiel ist hd (map f (map g [1..100])),
wobei f,g Funktionen sind, die Zahlkonstanten als Ein- und Ausgabe haben. In der Normalordnung wird vom Ausdruck [1..100] nur das erste Element ausgewertet, d.h. bis 1: [2..100], usw. Das Ergebnis ist
hd ((f (g 1)) : (map f (map g [2..100]))). Das ergibt (f (g 1)) und
danach wird dieser Ausdruck ausgewertet.
In der Anwendungsordnung wird zunächst der Ausdruck [1..100] zur Liste der
Länge 100: [1,2,3,...,100] ausgewertet. Am Ende hat man den Ausdruck
hd [a1,...,a100] wobei ai das Ergebnis von f (g i) ist. Das Ergebnis ist
ai.
Es gilt folgender Satz (Church-Rosser) der in der Literatur unter Existenz einer
Standardreduktion für den Lambda-Kalkül bekannt ist. Wir formulieren ihn
leicht verallgemeinert.
Satz 5.4.39 Sei s ein geschlossener Ausdruck. Wenn es einen Ausdruck t in
WHNF gibt mit s ∼c t, dann gilt s⇓.
Beweis. Unsere Definitionen sind auf diesen Fall zugeschnitten: Nehme den leeren Kontext. Dann folgt aus s ∼c t und t⇓ dass auch s⇓
2
Insbesondere heißt das:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
33
Aussage 5.4.40 Wenn für einen Term t die Reduktion bzgl. irgendeiner Strategie (z.B. Anwendungsordnung) terminiert, dann auch die Reduktion in Normalordnung. D.h. jede Strategie (auch die Anwendungsordnung) ist strikter als
die Normalordnung.
Beweis. Folgt aus der Annahme, dass S-Normalformen auch WHNFs sind, dass
Reduktionen ∼c erhalten, und aus dem Standardisierungs-Satz.
2
Aussage 5.4.41 Die Normalordnungsreduktion ist eine maximale Strategie bzgl
der Relation ist strikter als“ , wobei wir die striktere Strategie als kleiner an”
sehen.
Beweis. Das folgt aus dem Standardisierungssatz.
Beispiel 5.4.42 Zu verschiedenen Reihenfolgen der Auswertung von Ausdrücken.
Wertet man (quadrat(3 ∗ 3)) aus, so kann man das auf drei Weisen machen:
(quadrat(3 ∗ 3)) → quadrat 9 → 81 oder
(quadrat(3 ∗ 3)) → ((3 ∗ 3) ∗ (3 ∗ 3)) → (9 ∗ (3 ∗ 3)) → (9 ∗ 9) → 81 oder
(quadrat(3 ∗ 3)) → ((3 ∗ 3) ∗ (3 ∗ 3)) → ((3 ∗ 3) ∗ 9) → (9 ∗ 9) → 81
Bemerkung: Funktionale Programmiersprachen, die die Anwendungsordnung als
Auswertungsstrategie haben, nennt man strikte FPS, solche die die Normalordnung benutzen, nennt man auch nicht-strikte FPS. Der Begriff kommt eigentlich
aus der denotationalen Semantik der Programmiersprachen, wird aber auch für
die passende operationale Semantik verwendet.
5.5
Übersetzungen von Haskell nach KFPTS
nach KFPT nach KFP
Wir betrachten jetzt die Übersetzungen
Haskell → KFPTS → KFPT → KFP
und deren Eigenschaften. Da wir die Sprachen, die Reduktion und die entsprechenden WHNFs definiert haben, haben wir auch einen jeweiligen Begriff von
Terminierung, kontextueller Approximation und kontextueller Äquivalenz. Die
Notationen der Begriffe versehen wir mit den jeweiligen Indizes KFPTS, KFPT,
KFP wobei wir KFP als Index weglassen.
Definition 5.5.1 Sei τ eine Übersetzung von P1 nach P2 , von der wir annehmen, dass sie Kontexte und Terme übersetzt, und dass sie modular ist, d.h.
τ (C[s]) = τ (C)[τ (s)] für alle C, s. Insbesondere heißt das, dass für alle Zerlegungen eines Terms t in C[s] diese Eigenschaft gilt, so dass eine Übersetzung
jedes Konstrukt getrennt übersetzen muss.
Wir betrachten folgende Eigenschaften:
34
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
• τ erhält Terminierung, gdw. für alle s, t in P1 gilt:
s⇓1 ⇔ τ (s)⇓2
• τ ist korrekt bzgl. der kontextuellen Ordnung, gdw. für alle s, t in P1 gilt:
s ≤1 t ⇐ τ (s) ≤2 τ (t)
• τ ist vollständig bzgl der kontextuellen Ordnung, gdw. für alle s, t in P1
gilt:
s ≤1 t ⇒ τ (s) ≤2 τ (t)
• τ erhält kontextuelle Ordnung, gdw. wenn τ korrekt und vollständig bzgl
der kontextuellen Ordnung ist, d.h. wenn ür alle s, t in P1 gilt:
s ≤1 t ⇔ τ (s) ≤2 τ (t)
Die theoretisch sauberste Übersetzung / Implementierung ist eine, die die kontextuelle Ordnung erhält. Normalerweise hat man aber diese Eigenschaft nicht,
sondern nur die Erhaltung der Terminierung und die Korrektheit bzgl. der kontextuellen Ordnung.
Die Korrektheit der Übersetzung bzgl. der kontextuellen Ordnung ist ausreichend, um Überlegungen zur Gleichheit von Ausdrücken bzw. Korrektheit von
Programmtransformationen in die Zielsprache (meist die Kernsprache) zu verlagern. Damit kann man zwar einige Gleichungen verlieren, aber hat ein gutes
Werkzeug, um solche Gleichungen in einer einfacheren Sprache zu zeigen.
Folgendes Bild zeigt den Normalfall bei Implementiewrungen. Sie sind nicht
surjektiv, d.h. ewrreichen nicht jedes mögliche Programm. Das ist normalerweise
auch zwingend, denn in der Kernsprache hat man meist mehr Manipulationsund Testtmöglichkeiten. Das bedeutet auch, dass man z.B. mit den Mitteln von
P2 evtl. implementierte P1 -Programme unterscheiden kann, die innerhalb des
Bildes von P1 , d.h. mit den implementierbaren Tests nicht zu unterscheiden
sind.
P1
P2
P3
Aussage 5.5.2 Ist die Übersetzung τ von P1 nach P2 korrekt bzgl. kontextueller
Ordnung, dann gilt folgendes:
Seien s, t P1 -Terme, und gilt nach Übersetzung τ (s) ∼2 τ (t), dann gilt auch
s ∼1 t.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
35
Wir betrachten auch die Eigenschaften, die sich von der Sprache P1 ins Bild von
P1 in der Sprache P2 übertragen: Hier gilt, dass die Erhaltung der Terminierung
eine einfache, aber wirkungsvolle Bedingung ist:
Aussage 5.5.3 Erhält die Übersetzung τ von P1 nach P2 die Terminierung,
dann gilt, wenn man τ 0 als die Übersetzung von P1 in das Bild τ (P2 ) definiert:
τ 0 erhält die kontextuelle Ordnung (bzgl. τ (P2 )).
Beweis. Seien s, t P1 -Terme mit s ≤c,1 t, sei C2 ein Kontext in τ (P2 ), so
dass C2 [τ (s)]⇓. Dann existiert ein Kontext C1 mit τ (C1 ) = C2 . Damit gilt
wegen der Modularität von τ : τ (C1 [s]) = C2 [τ (s)], und wegen der Erhaltung der
Terminierung: C1 [s])⇓. Da s ≤c,1 t, gilt auch C1 [t]⇓, und somit auch τ (C1 [t])⇓,
und wegen C2 = τ (C1 ) auch Cs [τ (t)])⇓.
Seien s, t P1 -Terme mit τ (s) ≤c,1 τ (t) und sei C1 ein Kontext mit C1 [s]⇓. Dann
gilt τ (C1 )[s])⇓ wegen der Erhaltung der Terminierung. Wegen τ (s) ≤c,1 τ (t)
und τ (C1 )[τ (s)])⇓ auch τ (C1 )[τ (t)])⇓ und somit auch τ (C1 [t])⇓. Erhaltung der
Terminierung impliziert dann C1 [t])⇓.
2
Normalerweise kann man die Erhaltung der Terminierung leicht nachweisen,
wenn die Reduktionen in P1 und P2 unter τ (fast) erhalten bleiben:
∗
D.h. wenn s → t auch τ (s) −
→ τ (t) impliziert und τ (s) −
→ τ (t) auch s → t
impliziert, und für die entsprechenden Normalformen (WHNFs) gilt, dass wenn
s eine WHNF in P1 ist, dann gilt τ (s)⇓ und wenn τ (s) eine WHNF in P2 ist,
dann gilt auch s⇓.
Diese Kriterien sind normalerweise erfüllt, wenn die Übersetzung aus operationaler Sicht in Ordnung ist“. Sind diese Kriterien nicht erfüllt, dann braucht
”
man eine kompliziertere Argumentation.
5.5.1
KFPT nach KFP
Erstes Beispiel einer Übersetzung ist die Kodierung τ von KFPT-Ausdrücken
in KFP. Hierbei wurden nur die case-Ausdrücke verändert:
case s {p1 -> τ (t1 ); . . . ; pn ->τ (tn )}.
wird unter τ zu
case τ (s) {p1 -> τ (t1 ); . . . ; pn ->τ (tn ); ->bot}. Diese Übersetzung ist modular.
Aussage 5.5.4 Die Übersetzung von KFPT nach KFP erhält die Terminierung, und ist korrekt bzgl. kontextueller Ordnung.
Beweis. Die Erhaltung der Terminierung ist einfach zu sehen, da man Normalordnungsreduktionen direkt übertragen kann.
Um die Korrektheit bzgl. kontextueller Ordnung zu sehen, muss man folgendermaßen vorgehen:
Seien s, t KFPT-Terme und sei τ (s) ≤c,KF P τ (t). Um s ≤c,KF P T t zu zeigen,
sei C ein KFPT-Kontext und sei C[s]⇓KF P T . Der Kontext kann in einen KFPKontext C 0 übersetzt werden, so dass folgendes gilt: τ (C[s]) = C 0 [τ (s)] und
somit C 0 [τ (s)]⇓KF P . Daraus folgt wegen der Voraussetzung τ (s) ≤c,KF P τ (t),
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
36
dass C 0 [τ (t)]⇓KF P , und jetzt wieder mit der Erhaltung der Terminierung, dass
C[t]⇓KF P T .
2
Überraschend ist, dass der relative kleine Unterschied zwischen KFP und KFPT
schon dazu führt, dass die Übersetzung nicht mehr vollständig bzgl kontextueller
Ordnung ist:
Aussage 5.5.5 Die Übersetzung von KFPT nach KFP ist nicht vollständig
bzgl. der kontextuellen Ordnung.
Beweis. Betrachte den KFPT-Kontext D[] = λf . caseBool (f []) {True →
True; False → True} und die beiden Ausdrücke
s0 = D[(λx . ⊥)] und t0 = D[⊥].
Behauptung: In KFPT sind diese Ausdrücke nicht unterscheidbar. D.h.
s0 ∼c,KF P T t0 . Sei dazu C ein beliebiger KFPT-Kontext. Wir argumentieren,
dass beide Ausdrücke C[s0 ], C[t0 ] dasselbe Resultat bzgl. Terminierung ergeben. Wir machen eine Reduktion von C[s0 ] und C[t0 ] mit Markierungen, wobei
sowohl der case-Ausdruck in D markiert ist als auch der Ausdruck λx . bot und
bot.
Es genügt die Überlegung, dass für alle r die Resultate der Normalordnungsreduktion von s0 r und t0 r immer gleich sind. Es wird zunächst r (λx . bot)
reduziert, parallel dazu r (bot). Falls die Normalordnungsreduktion den markierten Term (λx . bot) oder (bot) auswertet, ist das Ergebnis Nichtterminierung,
also äquivalent zu bot, da es entweder in einem KFPT-case als erstes Argument
vorkommt, oder in Funktionsposition in einer Applikation. Wenn die Ausdrücke
nicht ausgewertet werden, dann ist das Ergebnis auf beiden Seiten vergleichbar. Da danach ein caseBool auf das Ergebnis gemacht wird, ist das entweder
ebenfalls bot, oder falls es einer der Werte True oder False ist, das Ergebnis
True.
D.h. die Ausdrücke s0 , t0 sind in KFPT nicht unterscheidbar.
Behauptung: s0 , t0 sind unterscheidbar in KFP.
Dazu sei strict der in KFP definierbare Operator. Als Kontext nehme C :=
([] (strict(λx . True))).
Das ergibt für s0 das Resultat True, da λx . bot in WHNF.
Für t0 ergibt sich bot, da obige Funktion den Versuch startet, bot auszuwerten.
2
Der obige Fall ist eigentlich der allgemeine Fall von Übersetzungen.
Der Unterschied zwischen den beiden Sprachen ließe sich beheben durch
Veränderung der Sprache KFPT insbesondere ein anderes case. Diese caseVeränderung machen wir jedoch nicht, da das eher eine künstliche Reparatur
wäre, und nicht zum Verständnis beiträgt.
Wir werden aber, um die Übersetzungskette von Haskell nach KFP der Praxis
anzupassen, auch die Übersetzungskette
Haskell → KFPTS+seq → KFPT+seq → KFP
betrachten
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.5.2
37
KFPT+seq nach KFP
Wenn wir seq zu KFPT hinzunehmen, gilt immer noch die Korrektheit der
Übersetzung τ bzgl kontextueller Äquivalenz. Als Übersetzung nehmen wir wieder die oben angegebene und die Darstellung von seq in KFP. Aber obiges
Gegen-Beispiel zur vollen Korrektheit wird ungültig. Trotzdem ist (das angepasste) τ nicht vollständig bei der Übersetzung von KFPT+seq nach KFP:
Beispiel 5.5.6 Sei
s0 = \f -> if (f True)
then (if (f Nil) then bot else True)
else bot
t0 = \f -> if (f Nil)
then (if (f True) then bot else True)
else bot
Diese Funktionen s0 , t0 sind in KFPT+seq nicht unterscheidbar, da (f True)
und (f Nil) nicht verschiedene (terminierende) Boolesche Werte liefern
können, und haben sie jeweils das gleiche Verhalten. Wenn die Funktion f unterschiedliche Werte liefern soll, dann muss sie das Argument mittels eines
caseanalysieren. Dann muss aber eins der Ergebnisse bot sein, denn das case in
KFPT ist getypt.
In KFP sind sie unterscheidbar, da man ein solches f f leicht definieren kann:
das case in KFP kann bei True den Wert True zurückgeben, und bei Nil den
Wert False. In diesem Fall liefert (s0 f f ) den Wert True, während (t0 f f ) zu
bot auswertet, d.h. keinen Wert ergibt.
Das bedeutet, dass die Definitionsmöglichkeit für den Operator strict bzw.
seq nicht der eizige Unterschied ist, sondern dass die Typisierung auch noch
einen Unterschied ausmacht: Durch die Typisierung hat man weniger Kontexte
in KFPT.
5.6
Übersetzung KFPTS nach KFPT
Wir untersuchen in diesem Kapitel den Zusammenhang der Reduktion zwischen
KFPT und KFPTS.
Es gibt folgende wesentlichen Unterschiede:
• In KFPT kann man Beta-Reduktion bereits bei einem Argument anwenden, in KFPTS die SK-Beta-Reduktion erst, wenn ausreichend viele Argumente vorhanden sind.
• In KFPTS gibt es allgemeine Rekursion. In KFPT gibt es keine (direkte)
Rekursion.
Wir geben zwei Übersetzungen τ und σ an:
• τ : KF P T S → KF P T .
• σ : KF P T → KF P T S wobei lambda-freie Superkombinatorausdrücke
entstehen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
38
Um die Betrachtungen zur Übersetzungsfunktion zu vereinfachen, nehmen wir
an, dass die Typen und Konstruktoren im KFPT und im KFPTS-Programm
jeweils dieselben sind.
5.6.1
Von KFPT nach KFPTS: Lambda-Lifting
Bei der Übersetzung schießen wir etwas über das Ziel hinaus: wir zeigen, wie
man ein lambda-freies KFPTS-Programm aus einem KFPT-Programm erhalten
kann.
Erster Schritt der Übersetzung ist die Elimination von lokalen Abstraktionen,
die lokal freie Variablen enthalten. Dies wäre nicht direkt in lambda-freies
KFPTS übersetzbar.
Definition 5.6.1 Sprechweisen in der Sprache KFPT (im Lambda-Kalkül):
• Ein Kombinator ist ein geschlossener Ausdruck.
• Eine maximale Abstraktion t0 in einem Ausdruck t ist entweder t selbst,
wenn t eine Abstraktion ist, oder ein Unterausdruck (eine Abstraktion)
dessen direkter Oberterm keine Abstraktion ist.
• Ein Superkombinator t ist ein geschlossener KFP-Ausdruck e, so dass alle
maximalen Abstraktionen in e, die echte Unterterme von t sind, ebenfalls
Superkombinatoren sind.
• Ein Ausdruck, dessen sämtliche maximale Abstraktionen Superkombinatoren sind, heißt Superkombinatorausdruck.
Beispiel 5.6.2
• λx1 , x2 , . . . xn . xi ist ein Superkombinator.
• λx . (x λy . x) ist kein Superkombinator, da die maximale Abstraktion
λy . x eine freie Variable enthält.
• (λx.λy.(y (λx.x))) ist ein Superkombinator.
• case Nil {(x : xs) → x; Nil → Nil} ist ebenfalls ein Superkombinator.
Allerdings einer, der keine Argumente hat.
Die Regel Lambda-Lifting verwenden wir jetzt, um aus jedem geschlossenen KFPT-Ausdruck einen kontextuell äquivalenten KFPTSuperkombinatorausdruck zu machen.
Sei t ≡ C[t0 ] wobei t0 eine maximale Abstraktion in t ist, C nichttrivial, jeder echte Unterterm von t0 , der eine maximale Abstraktion
ist, ein Superkombinator ist, und y frei ist in t0 .
Dann:
ll
C[t0 ] −
→ C[((λz . (t0 [z/y])) y)]
wobei z eine neue Variable ist.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
39
Offenbar ist diese Regel korrekt, was direkt aus den Sätzen zur kontextuellen Gleichheit in KFP (KFPT) folgt, da sich diese Transformation mit BetaReduktion wieder rückgängig machen lässt.
Außerdem terminiert die mehrfache Anwendung der Regel und erzeugt einen
Superkombinatorausdruck:
Lemma 5.6.3 Jeder geschlossene KFPT-Ausdruck kann mittels endlich vieler
Lambda-Liftings in einen KFPT-Superkombinatorausdruck transformiert werden, so dass kontextuelle Äquivalenz erhalten bleibt.
Beweis. Die Regel ist immer anwendbar, wenn t kein Superkombinatorausdruck
ll
ist. Also zeigen wir, dass die fortgesetzte Regelanwendung von −
→ terminiert.
Am einfachsten ist das folgendermaßen einzusehen: Jeder Superkombinator im
Ausdruck wird durch einen Namen abgekürzt. Das ändert nichts an der Ausführbarkeit und Ausführung der Regel. Eine einfache Anwendung auf t0 vermindert
die Anzahl der freien Variablen in t0 , und wenn der Ausdruck ein Superkombinator ist, dann kann er abgekürzt werden. Insgesamt wird danach die Anzahl der
λ’s weniger, auch wenn man die vom Lambda-Lifting neu eingeführten mitzählt.
Das terminiert, da nach endlich vielen Anwendungen keine Abstraktionen mehr
vorhanden sind; alle sind in Abkürzungen ausgelagert.
2
Superkombinatorübersetzung KPFT → KFPTS
Ein KFPT-Ausdruck wird mittels σ nach KFPTS übersetzt, indem man
zunächst mit Lambda-Lifting daraus einen Superkombinator macht und dann
von innen her die Superkombinatorausdrücke durch neu definierte Superkombinatoren ersetzt.
D.h. man fügt eine Definition
name := E
Wenn man das noch syntaktisch etwas umschreibt in
name x1 . . . xn := E 0
wobei E ≡ λx1 . . . xn . E 0 , dann hat man KFPTS-Definitionen.
Die Stelligkeit eines Superkombinators legen wir fest als die Anzahl der im
Lambda-Präfix des Superkombinators gebundenen Variablen.
Die Vergleichbarkeit der Normalordnungsreduktionen ist folgendermaßen:
Eine SK-Beta-Reduktion lässt sich ausdrücken durch mehrere Beta-Reduktionen
in KFPT. Die Anzahl entspricht genau der Stelligkeit des zugehörigen Superkombinators.
Umgekehrt ist es genauso, entweder ist es eine Beta-Reduktion, oder man kann
mehrere Normalordnungsreduktionen jeweils zusammenfassen als eine SK-BetaReduktion, oder in KFPTS gibt es keine weitere Normalordnungsreduktion, da
zuwenig Argumente da sind, aber in KFPT kann man noch einige Reduktionen
machen, bis eine Abstraktion erreicht ist.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
40
Ein KFPT-Superkombinatorausdruck ist problemlos nach KFPTS zu übersetzen, wobei alle Definitionen und der Ausdruck lambdafrei sind:
Es gilt:
Aussage 5.6.4 Sei t ein geschlossener KFPT-Ausdruck. Dann ist t⇓ ⇔
σ(t)⇓KF P T S
D.h Diese Übersetzung σ ist korrekt unter dem Kriterium der Erhaltung der
Terminierung.
Die Erhaltung der kontextuellen Ordnung (und damit auch der Äquivalenz) von
Ausdrücken bei der Übersetzung σ gilt ebenfalls:
Aussage 5.6.5 Seien s, t geschlossene KFPT-Ausdrücke. Dann gilt
s ≤c.KF P T t ⇔ σ(s) ≤c,KF P T S σ(t).
Beweis. Die Richtung
s ≤c.KF P T t ⇐ σ(s) ≤c,KF P T S σ(t)
gilt, da KFPT-Kontexte auch KFPTS-Kontexte sind. Für die Umkehrung
braucht man noch die Übersetzung der KFPTS-Kontexte, die rekursive Superkombinatoren enthalten.
2
Bemerkung 5.6.6 Die Übersetzung σ, obwohl sie so einfach ist, ist nicht bzgl
Reduktionsäquivalenz“korrekt: WHNFs entsprechen sich nicht bei Übersetzung.
”
Die Reduktion lässt sich übersetzen, bis auf die Reduktion von KFPT-WHNF zu
KFP-WHNF.
Beachte, dass man die Betrachtung auch für die Übersetzung von KFPT in
lambda-freies KFPTS machen kann. In lambda-freiem KFPTS bräuchte man
eine erweiterte Kontext-Definition: Um genügend viele Kontexte zu haben, muss
man erlauben, dass man vor der Kontextbildung erst noch Superkombinatoren
definieren kann.
5.6.2
Übersetzung KFPTS nach KFPT
Für die Übersetzung KFPTS nach KFPT, insbesondere von rekursiven Superkombinatoren, benötigen wir etwas mehr Aufwand, da man Rekursion nicht
direkt nach KFPT übersetzen kann.
Das Stichwort heißt: Fixpunkte und Fixpunktkombinatoren.
Die Auflösung der Rekursion ist möglich mit einem Superkombinator Y , der
folgendes leistet:
Für alle Abstraktionen F gilt in KFPT: Y F ∼c F (Y F ) D.h. (Y F ) ist ein
Fixpunkt von F . Man nennt Y auch einen Fixpunktkombinator.
Solch einen Kombinator gibt es (sogar in KFP):
Y = λf.(λx.f (x x))(λx.f (x x))
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
41
Wir reduzieren (Y F ):
(Y F ) → (λx.F (x x))(λx.F (x x))
→ F (x x)[x/(λx.F (x x))]
= F ((λx.F (x x))(λx.F (x x))
Beachte,
dass
ein
Fixpunktkombinator
in
strikten
funktionalen
Programmiersprachen
leicht
abgewandelt
werden
muss:
Y_str = \f. (\x . f (\z. x x z)) (\x . f (\z. x x z)), da sonst
die strikte Argumentauswertung ein nichtterminierendes Auswerten erzwingt
∗
∗
nach folgendem Schema: (Y F ) a −
→ F (Y F )a −
→ F (F (Y F ))a . . ., was durch
∗
obige Abänderung vermieden wird: (Ystr F ) a −
→ F (λz.Ystr F z)a . . ..
Satz 5.6.7 Für alle Abstraktionen F gilt Y F ∼c F (Y F ).
Beweis. Folgt aus den Sätzen über kontextuelle Gleichheit der Reduktion in
KFPT.
2
Diese Äquivalenz kann man ausnutzen, um Rekursion aus den KFPTSDefinitionen von Superkombinatoren zu eliminieren.
Elimination der einfachen Rekursion:
Sei H x_1 ... x_n = e eine rekursive Definition in KFPTS. Wir nehmen an,
dass e nur Vorkommen von H enthält, aber keine anderen Superkombinatoren.
Dann erzeuge den Ausdruck
G = λf.λx1 . . . λxn .e[f /H]
in dem der Superkombinatorname abstrahiert ist.
Danach definieren wir F = (λx.G(x x)) (λx.G(x x)). Das entspricht F = Y G
nach einer Reduktion. Dies ist ein Ausdruck in KFPT ohne Vorkommen eines
Superkombinatornamens.
Wir zeigen, dass F zu H äquivalent ist in folgendem Sinn:
Die Terminierung ändert sich nicht, wenn man vom KFPTS-Ausdruck mit H
zu dem KFPT-Ausdruck mit F übergeht.
Definition 5.6.8 Sei τH die Übersetzung, die in jedem KFPTS-Ausdruck die
Vorkommen von H durch F ersetzt.
Aussage 5.6.9 Sei in KFPTS nur eine einzige einfache rekursive Definition
(H) wie oben. Sei t ein geschlossener KFPTS-Ausdruck, und sei τH wie oben
definiert. Dann ist t⇓KF P T S ⇔ τ (t)⇓
Beweis. Wir haben schon gesehen, dass der Unterschied zwischen der KFPTNormalordnungsreduktion und der KFPTS-Normalordnungsreduktion nicht wesentlich ist. Man kann zeigen, dass sich Normalordnungsreduktion mit τH
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
42
überträgt. Der einzig interessante Fall, der einen Unterschied machen könnte,
tritt ein, wenn der Normalordnungsredex gerade H a1 . . . an ist. Dann reduziert
dieser Term auf der KFPTS-Seite zu e[a1 /x1 , . . . an /xn ]. Auf der KFPT-Seite
ergibt sich in diesem Fall:
F a1 . . . an
→
→no
∗
−
→no
(λx.G (x x))(λx.G (x x)) a1 . . . an
G((λx.G (x x))(λx.G(x x))) a1 . . . an
e0 [(λx.G (x x))(λx.G (x x))/H; a1 /x1 , . . . , an /xn ]
Das ist gerade die Übersetzung τH (e[a1 /x1 , . . . an /xn ]). Da sich die WHNF entsprechen (bis auf KFPT-Reduktionen kurz vor Erreichen der WHNF), ergibt
sich mit Induktion nach der Anzahl der KFPTS-Normalordnungsreduktionen
die Behauptung.
2
Der Fall, dass die Rekursion verschränkt ist, bzw. dass die Verwendung der
Superkombinatornamen nicht eingeschränkt ist, ist einfach zu modellieren, da
es Konstruktoren gibt:
Wenn in einem KFPTS-Programm die (rekursiven) Superkombinatoren
S1 , . . . , Sn definiert sind, d.h. das Programm ist:
S1 x1 ... x_m_1
...
Sn x1 ... x_m_n
main
= e1
= en
= e
Wir nehmen wir an, dass die Zahlen 1, . . . , n, evtl. als extra Konstanten,
verfügbar sind.
Man macht eine erste Übersetzung, indem man alle Kombinatoren in eine rekursive Funktion einbaut:
F i := case_int i of {1 -> \x1,...,x_m_1.e1[F 1/S1, ... F n/Sn];
... ;
n -> \x1,...,x_m_n.en[F 1/S1, ... F n/Sn]}
Danach kann man die Rekursion von F eliminieren, indem man das obige Verfahren anwendet und Y (λf, i.ef [f /F ]) bildet, wobei ef der Rumpf des Kombinators F ist. Das funktioniert allerdings nur in einem weitgehend ungetypten
Kalkül.
Am Ende wird im Rumpf e des Kombinators main die entsprechende Ersetzung
gemacht, d.h. Si wird ersetzt durch Y (\lambda f,i. e_f[f/F])~i.
Diese Methode ist nicht elegant, da sie einerseits neue Konstruktoren einführt,
und andererseits beim Hinzufügen von unabhängig definierten Kombinatoren die
Übersetzung ändert, da jeweils das ganze Programm übersetzt werden muss.
Beispiel 5.6.10 Angenommen, wir haben zwei verschränkt rekursiv definierte
Funktionen f, g.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
43
f x = f (f x) (g x)
g x = g x (f x)
main = e
Dann definieren wir eine Funktion G:
G i
=
case_int i
{1 ->
\x. (F 1 (F
2 -> \x. (F
1 x) (F 2 x));
2 x (F 1 x)); ... }
Bildet man jetzt Y G, und ersetzt alle Vorkommen von f durch Y G 1 und alle
Vorkommen von g durch Y G 2, dann hat man die Rekursion eliminiert.
Denn g a wird ersetzt durch Y G 2 a. Dies wird durch Reduktion
zu
G ((\x. G(x x))((\x. G(x x)))) 2 a
und
wenn
wir
((\x. G(x x))((\x. G(x x)))) mit GG abkürzen, dann wird dies nach
Reduktion zu (GG 2 x (GG 1 x))).
Man sieht auch, dass es nicht nötig ist, die Funktionen unter einem Konstruktor
zusammenzufassen. Es genügt eine Fallunterscheidung.
Definition 5.6.11 Die Übersetzung τ : KF P T S → KF P sei so definiert, dass
Superkombinatoren, die nicht rekursiv sind, direkt nach KFPT übersetzt werden.
Rekursive Superkombinatoren werden in Gruppen zusammengefasst, so dass sich
Funktionen aus verschiedenen Gruppen nicht aufrufen. Danach kann man die
Übersetzung gruppenweise durchführen. Dies garantiert, dass man neue Definitionen zu einem Programm hinzufügen kann, ohne dass sich die Übersetzung
bereits vorhandener ändert.
Folgendes Diagramm gibt den Zusammenhang wieder zwischen Normalordnung
und Übersetzung:
Wenn t nach τ (t) übersetzt wird, und man einen Normalordnungsreduktionsschritt von t nach t0 machen kann, dann kann man τ (t) in KFP-Normalordnung
auf τ (t0 ) reduzieren, wie im Diagramm angegeben.
t
τ
/ τ (t)
τ
/ τ (t0 )
no,∗
no
t0
Insgesamt haben wir damit eine Übersetzung τ von KFPTS-Superkombinatoren
nach KFPT, so dass die Terminierung und die kontextuelle Approximation erhalten bleiben, D.h. Wir können sagen, dass KFPT und KFPTS (bzgl. τ ) äquivalent sind:
Satz 5.6.12
• t⇓KF P T S ⇔ τ (t)⇓
• s ≤c,KF P T S t ⇔ τ (s) ≤c τ (t)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
44
Begründung des Satzes. Dass die Terminierung erhalten bleibt, folgt aus den
Überlegungen zur Übertragung der Reduktion.
Die Übersetzung τ ist die Inverse zu σ: Es gilt τ (σ(t)) ∼c t.
Dass die kontextuelle Ordnung erhalten bleibt, kann man folgendermaßen sehen:
1. s ≤c,KF P T S t. Sei C ein KFPT-Kontext, so dass C[τ (s)]⇓. Dieser Kontext
ist auch ein KFPTS-Kontext, so dass τ (C[s]) = C[τ (s)]. Damit gilt C[t]⇓,
und somit auch τ (C[t])⇓.
2. Sei τ (s) ≤c τ (t). Um s ≤c,KF P T S t zu zeigen, sei C ein KFPTS-Kontext,
und sei C[s]⇓. Diesen kann man mit τ nach KFPT übersetzen, und erhält
dann τ (C[t])⇓. Mit 1 erhält man dann auch t⇓.
Satz 5.6.13 KFPTS und KFPT sind äquivalent bzgl. der Übersetzungen τ und
σ.
Hier fehlt noch die eine Richtung zu σ. Das geht aber genauso wie bei τ .
5.6.3
Übersetzung von Haskell nach KFP: Zusammenfassung
Die Übersetzung von Haskell nach KFPTS+seq ist durch die reiche Syntax
und die Möglichkeiten in Haskell etwas aufwändig. Wir machen die folgenden
Vereinfachungen:
• Basisdatentypen und Funktionen nehmen wir als in Haskell implementiert
• List comprehensions nehmen wir an als übersetzt mittels Listenfunktionen.
• Patterns sollen als geschachtelte cases aufgelöst sein.
• monadische Programmteile ebenfalls als in purem Haskell
Die dann erzeugten Programme kann man dann durch Weglassen der Typen
nach KFPTS+seq übersetzen.
Es ergeben sich die gleichen Eigenschaften in der Übersetzungskette
Haskell → KFPTS+seq → KFPT+seq → KFP
Haskell → KFPTS+seq
D.h. KFPTS+seq → KFPT+seq
KFPT+seq → KFP
korrekt, aber vermutlich nicht vollständig
vollständig
korrekt, aber nicht vollständig
Beispiel 5.6.14 Wir geben ein Gegenbeispiel zur Vollständigkeit der ersten
Transformation an, wenn man statt Haskell KFPTS nimmt und nur polymorphe Typisierung verlangt, und keine Milner-Typisierung: Wir haben die kontextuelle Gleichheit in Haskell nicht formal exakt genug definiert, so dass wir
ein Beispiel angeben, das robust ist unter verschiedenen Varianten. Die Idee
ist, dass g testet, ob die eingegebene Funktion f die Argumente True, Nil und
True, (Cons bot bot) als auch False, True und False, False unterscheiden kann.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
45
g f = if (if (f True Nil) then if (f True (Cons bot bot))
then bot else True
e
else bot)
&&
(if (f False True) then if (f False False)
then bot else True
else bot)
then True else bot
Die Behauptung ist, dass g in KFPTS+seq+poly“ äquivalent zu ⊥ ist, da man
”
keinen polymorph getypten Kontext angeben kann, der beide unterscheidet. Bzw.
für alle KFPTS+seq+poly“-Funktionen f, die polymorph getpyt sind, kann sich
”
nur bot bei der Auswertung von g f ergeben, d.h. es gibt keinen Unterschied.
Aber in KFPT+seq kann man leicht eine Funktion f angeben, so dass g f den
Wert True liefert: zwei geschachtelte cases sind dazu nötig.
Beispiel 5.6.15 Auch unter Milner-Typisierung gibt es Gegenbeispiele:
s x y =
t x y
if isBool x then
=
if isBool x
then
(seq y True) else isBool y
isBool
y
else isBool y
isBool x = case_Bool x of True->True, False-> True
Dann ist s ∼ t in Milner-getyptem KFPTS + seq, aber nicht in in KFPTS +
seq, da man die Argumente True,Nil nehmen kann, bei der sich die Funktionen
unterscheiden.
In der Summe ergibt sich, dass die Übersetzung von Haskell nach KFP korrekt
ist.
Wir können somit Gleichungen s ∼c t in Haskell zeigen, indem wir diese nach
KFP übersetzen und dort nachweisen. Bei Widerlegungen von Gleichungen in
KFP muss man sich vergewissern, dass die Widerlegung auch in Haskell funktioniert, bzw. man muss ein Gegenbeispiel in Haskell angeben: I.a. ist die Bedingung hier nur, dass das Gegenbeispiel polymorph getypt sein muss.
5.7
Approximationen in KFP und Induktion
Auf der Ordnung ≤c in KFP kann man aufbauen, um mittels Approximation
kontextuelle Gleichheit von interessanten Ausdrücken in KFP zu zeigen. Da
wir bereits argumentiert haben, dass sich Gleichungen von KFP nach KFPTS
übertragen lassen, werden wir die Unterschiede ignorieren.
Beispiel 5.7.1
⊥ ≤c (S bot)
Term S (S (S
bei Auswertung
Wir hatten bereits in einem Beispiel vermutet, dass die Kette
≤c (S (S bot)) ≤c . . . gegen den unendlich verschachtelten
(... ))) konvergiert. Insbesondere, dass alle Ausdrücke, die
einen solchen Term produzieren, kontextuell gleich sind.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
46
Definition 5.7.2 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette. Der Ausdruck s
ist eine kleinste obere Schranke (lub) dieser Kette, gdw.
∀i : si ≤c s und für alle r gilt: (∀i : si ≤c r) ⇒ s ≤c r.
Dieser Begriff des lub reicht für unsere Zwecke nicht aus. Man muss noch etwas
mehr definieren:
Definition 5.7.3 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette. Der Ausdruck s
ist eine kontextuelle kleinste obere Schranke (club) dieser Kette, gdw.
∀C : C[s] = lubi (C[si ]). Wir notieren das als s = clubi (si ).
Die Begriffe lub und club sind nur eindeutig bis auf ∼c definiert.
Es gibt folgendes Kriterium für einen club einer aufsteigenden Kette:
Lemma 5.7.4 Sei s1 ≤c s2 ≤c . . . eine aufsteigende Kette, sei s ein Ausdruck.
Es gelte:
1. Für alle i : si ≤c s
2. Für alle Kontexte C gilt: C[s]⇓ ⇒ ∃i : C[si ]⇓
Dann gilt s = clubi (si ).
Beweis. Sei D ein Kontext. Wir wollen zeigen D[s] = lubi (D[si ]). Sei dazu C ein
weiterer Kontext und r ein Ausdruck mit ∀i : D[si ] ≤c r. Nach dem Kriterium
gilt: Wenn CD[s]⇓, dann existiert ein j mit CD[sj ]⇓, also wegen D[sj ] ≤c r gilt
dann auch C[r]⇓. Es gilt also: CD[s]⇓ ⇒ C[r]⇓. Da das für alle Kontexte C gilt,
haben wir D[s] ≤c r gezeigt.
2
In Mason, Smith, Talcott gibt es ein Beispiel, das zeigt, dass nicht jeder lub auch
ein club ist. Dort wird in etwa formuliert: “Anwendung ist bzgl lub unstetig.“
Bemerkung 5.7.5 Nicht jede aufsteigende Kette hat einen club in der Menge
der Ausdrücke. Z.B. kann man für jeden unendlichen String a1 a2 a3 . . . über
{0, 1} eine aufsteigende Kette bilden: bot ≤c a1 : bot ≤c a1 : a2 : bot ≤c a1 : a2 :
a3 : bot ≤c . Man kann mit den bisher vorgestellten Mitteln nachweisen, dass
der club im Falle der Existenz eindeutig ist, gerade der “unendlichen Liste“ a1 :
a2 : . . . entspricht und für verschiedene Strings verschiedene clubs produziert.
Allerdings ist die Menge der möglichen Ausdrücke abzählbar, während die Menge
der unendlichen Strings überabzählbar ist. Damit muss es aufsteigende Ketten
geben, die keinen club besitzen.
Man kann auch konkrete aufsteigende Ketten angeben ohne club indem man
nichtberechenbare Funktionen verwendet.
Definition 5.7.6 Ein Abbildung φ, die Ausdrücke auf Ausdrücke abbildet heißt
stetig, gdw
1. s ≤c t ⇒ φ(s) ≤c φ(t)
47
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
2. Sei si aufsteigende Kette mit clubi si = s. Dann ist clubi (φ(si )) = φ(s)
Aussage 5.7.7 Jeder KFP-Ausdruck ist eine stetige Abbildung. D.h. die Abbildung φt mit:
φt s := t s
ist stetig.
Beweis. Folgt direkt aus den Definitionen, da (t .) ein Kontext ist.
2
Definition 5.7.8 Zwei Ausdrücke s, t sind gleich bis auf Rechtstiefe n, wenn
jede Position, an der s, t verschiedene Markierung haben, einen Teilpfad der
Form |2 .{z
. . 2} mit m ≥ n hat. D.h. der Pfad von der Wurzel zur Position hat ein
m
Teilstück, das mindestens n mal nach rechts geht. Diese Definition machen wir
modulo α-Gleichheit.
Beispiel 5.7.9 Die Ausdrücke f (. . . (f a) . . .) und f (. . . (f b) . . .) unterschei| {z }
| {z }
8
8
den sich an der Markierung a, b, die eine Rechtstiefe von 8 hat. Somit sind die
beiden Terme gleich bis auf Rechtstiefe 8.
Lemma 5.7.10
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n, und s1 →no
s01 , s2 →no s02 , dann sind s01 , s02 gleich bis auf Rechtstiefe n − 1.
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n, und
s1 →no,m s01 , s2 →no,m s02 , dann sind s01 , s02 gleich bis auf Rechtstiefe n−m.
• Wenn zwei Ausdrücke s1 , s2 gleich sind bis auf Rechtstiefe n ≥ 1, und s1
ist eine WHNF, dann ist auch s2 eine WHNF.
Beweis. Die jeweiligen Normalordnungsredexe von s1 , s2 sind ebenfalls gleich
bis auf Rechtstiefe n.
Eine Beta-Reduktion verändert folgendes: ((λx.t1 ) r1 ) → t1 [r1 /x]. Die Ausdrücke r1 , r2 sind gleich bis auf Rechtstiefe n − 1. Die Ausdrücke t1 [r1 /x] und
t2 [r2 /x] sind gleich bis auf Rechtstiefe n − 1; Jeder Pfad, der in Rechtstiefe
≤ n − 1; verschiedene Markierungen findet, kann nicht ganz in t1 , t2 sein, sondern geht durch eine ehemalige Position eines x, und folgt dann (einer Kopie
von) r1 bzw r2 .
Für eine Case-Reduktion gilt das analog, da die Änderungen ebenfalls Ersetzungen sind.
2
Satz 5.7.11 Sei t ein (evtl. offener) Ausdruck, und sei t0 := ⊥, t1 = (t ⊥), ti =
t(ti−1 ). Dann ist Y t ∼c clubi (ti ). Außerdem ist Y t kleinster Fixpunkt von t.
Beweis.
48
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
• ti ist eine aufsteigende Kette: Mit Induktion nach i kann man zeigen, dass
ti aufsteigend ist: ⊥ ≤c t1 . Dann mit Induktion: aus ti−1 ≤c ti folgt mit
der Monotonie: t ti−1 ≤c t ti , d.h. ti ≤c ti+1 .
• ∀i : ti ≤c Y t. Es gilt ⊥ = t0 ≤c Y t.
Induktionsschritt: Sei tn ≤c Y t. Dann gilt auch t tn ≤c t(Y t), da ≤c eine
Präkongruenz ist. Das bedeutet aber gerade tn+1 ≤c Y t.
• Y t ≤c clubi (ti ). Wir weisen das Kriterium in Lemma 5.7.4 nach.
Sei C ein Kontext, so dass C[Y t]⇓ und sei i die Anzahl der notwendigen
Normalordnungsreduktionen. Dann gilt C[ti ]⇓. Dazu ersetzen wir C[Y t]
durch C[ti+1 (Y t)]. Dieser Term hat nach Aussage 5.3.5 nicht mehr als i
Normalordnungsreduktionen zur WHNF. Er ist gleich zu C[ti+1 ] bis auf
Rechtstiefe i + 1. Nach Lemma 5.7.10 ist die WHNF von C[ti+1 (Y t)]
gleich bis auf Rechtstiefe 1 zu dem Ausdruck, der nach i Normalordnungsreduktionen aus C[ti+1 ] entsteht. Das bedeutet, C[ti+1 ] ist ebenfalls eine
WHNF.
Jetzt können wir schließen, dass Y t ∼c clubi (ti ).
• Y t ist kleinster Fixpunkt von t: Sei f ein Fixpunkt von t. Dann gilt
t f ∼c f . Mit Induktion kann man leicht zeigen, dass ti ≤c f :
Es ist bot ≤c f ; und wenn ti ≤c f , dann auch t ti ≤c t f , d.h. ti+1 ≤c f .
Aus der Eigenschaft der lubs folgt jetzt Y t ≤c f . D.h. Y t ist der kleinste
Fixpunkt von t (bzgl. ≤c ).
2
Bemerkung Die Ausdrücke ti entsprechen der rekursiv definierten Funktion t,
wobei das erste Argument der Name der Funktion ist, und man die Rekursion
nach i Schritten abbricht, und dann bot evaluiert. D.h. eine rekursive Funktion
wird approximiert durch Funktionen, die jeweils immer mehr rekursive Aufrufe
machen dürfen.
Beispiel 5.7.12 Sei S ein einstelliger Konstruktor. Jetzt können wir zeigen,
dass alle Ausdrücke t, die einen unendlichen Ausdruck (S (S (S ...))), mittels Reduktion erzeugen, kontextuell äquivalent sind. Genauer: Ausdrücke t mit
der Eigenschaft: ∀n.∃t0 : S(. . . (S t0 ) . . .).
| {z }
n
Der Satz zeigt, dass (Y S) ∼c club(si ), wobei si = (S(. . . (S ⊥))). Damit erhält
| {z }
i
man auch, dass (Y S) ≤c t, da si ≤c t für jede Approximation si gilt. Es fehlt
noch der Nachweis t ≤c (Y S). Den kann man analog zum Beweis von Satz
5.7.11 ausführen, indem man mit der Rechtstiefe argumentiert.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.7.1
49
Nachweis von Gleichungen: Induktion
Definition 5.7.13 Ein (totales oder partielles) Prädikat P (·) ist eine Funktion
von Ausdrücken in die Menge {True, False}. Das Prädikat P (·) ist zulässig,
wenn für jede ≤c -aufsteigende Folge von Ausdrücken si gilt: Wenn ab einem
Index i0 gilt, dass für alle i ≥ i0 : P (si ) gilt, dann gilt auch P (clubi (si )).
Bei mehreren Argumenten ist P (x1 , . . . , xn ) zulässig, wenn für alle ≤c aufsteigenden Folgen si,j , j = 1, . . . , n folgendes gilt: Wenn es einen Index i0 gibt, so dass für alle i ≥ i0 P (si,1 , . . . , si,n ) gilt, dann auch
P (clubi (si,1 ), . . . , clubi (si,n )).
Will man eine Eigenschaft einer Definition oder eines Ausdrucks nachweisen, so
kann man dies mit einer speziellen Form der Induktion machen:
Fixpunktinduktion.
Sei P ein zulässiges Prädikat. Will man P (s) nachweisen, bzw.
∀x1 , . . . , xn .P (s), so kann man in vielen interessanten Fällen diesen
Nachweis führen, indem man eine Folge si angibt mit clubi si = s,
und P (si ) für alle i nachweist; d.h. P (si ) = True.
Da P zulässig ist, kann man daraus schließen, dass P (s) gilt.
Meist sind diese Prädikate Gleichungen zwischen Ausdrücken, wobei die Semantik der Gleichungen gerade ∼c ist.
Aussage 5.7.14 Folgendes sind zulässige Prädikate.
• P (x) ≡ x ∼c t für festes t
• P (x, y) ≡ x ∼c y
• P (x) ≡ x ≤c t für festes t
• P (x) ≡ t ≤c x für festes t
• P (x, y) ≡ x ≤c y
• P (x, y) ≡ x ≤c y
• P (x) ≡ t <c x für festes t
Beweis. Wenn ab einem i0 für alle i ≥ i0 : si ∼c t gilt, dann offenbar auch
club(si ) = t.
Zum Nachweis der Behauptung, dass x ≤c y ein zulässiges Prädikat ist, seien
si , ti jeweils ≤c -aufsteigende Ketten mit club. Sei i0 ein Index, so dass für alle
i ≥ i0 : si ≤c ti gilt. Dann gilt, dass si ≤c clubi (ti ). Aus der lub-Eigenschaft
folgt dann clubi (si ) ≤c clubi (ti ).
Ähnlich kann man die anderen Behauptungen beweisen
2
Beachte, dass P (x) ≡ x <c x für festes t kein zulässiges Prädikat ist.
Man kann weitere zulässige Prädikate bilden durch Zusammensetzen von anderen zulässigen, indem man die logischen Operatoren ∧, ∨, verwendet. Allerdings
ist not nicht erlaubt.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
50
Bemerkung 5.7.15 Das Prädikat “terminiert“ kann man in stetigen zusammengesetzten Prädikaten verwenden: z.B. das Prädikat
P (x) ⇔ (f x) terminiert
kann man in modifizierter Form durchaus verwenden, wenn man es als
(f x) >c bot darstellt. Denn wenn für alle i gilt: si >c bot, dann gilt auch
clubi (si ) >c bot. Damit das brauchbar ist, muss man evtl. den Anfang der Kette
weglassen.
Man kann auch das Prädikat “nichtterminiert verwenden, denn es lässt sich
”
durch P (x) := x ∼c ⊥ simulieren.
Zusammenfassend darf man (mindestens) zum Zusammensetzen von stetigen
Prädikaten verwenden:
• All-Quantor; und, oder.
• ∼c , ≤c . x terminiert“.
”
• KFP-Ausdrücke
Der Nachweis, dass P (si ) für alle i gilt, wird oft in Form einer Induktion durchgeführt. Oft kann man die Form der möglichen Argumente rekursiv beschreiben,
wobei man alle WHNFs aufzählt.
Eine Standardform der Induktion ist der Nachweis von Eigenschaften für
Funktionen, die Listenargumente haben.
Induktionsschema zum Nachweis von P (.) für Listen:
• Zeige P (⊥).
• Zeige P (N il).
• Zeige: P (xs) ⇒ P (s : xs).
Dann kann man schließen: P (s) gilt für alle Listen s.
Definition 5.7.16 Sei eine Funktion zur Berechnung der “Partial-Liste“ einer
Liste definiert als
partlist 0 xs = bot
partlist (n+1) [] = []
partlist (n+1) (x:xs) = x: partlist n xs
Beispiel 5.7.17 Die Liste l = [1..] hat als Partiallisten bot, 1:bot, 1:2:bot.
Lemma 5.7.18 Sei eine (unendliche) Liste l 6= ⊥ gegeben und sei li :=
partlist i l. Dann ist li aufsteigend bzgl. ≤c und clubi (li ) = l.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
51
Beweis. Wir nehmen an, dass sich die Liste l beliebig tief in Richtung “Tail“
auswerten lässt. Anderenfalls wäre der n-te Tail der Liste = bot, und die Behauptung leicht zu zeigen.
Es ist klar, dass li ≤c l. Um l = clubi li zu zeigen, müssen wir zeigen, dass für
jeden Kontext C[]: C[l]⇓ ⇒ ∃i : C[li ]⇓.
Wir verwenden dazu die Rechtstiefe. Sei C ein Kontext, so dass C[l]⇓. Sei i die
Anzahl der Normalordnungsschritte, um C[l] zu einer WHNF zu reduzieren. Wir
können annehmen, dass l so ausgewertet ist, dass l die Form a1 : a2 : . . . ai+1 :
ai+2 hat, wobei ai+2 auch unausgewertet sein kann. Sei li , definiert wie oben,
bereits so weit ausgewertet, dass li = a1 : a2 : . . . ai+1 : ⊥. Dann sind li und l
gleich bis auf Rechtstiefe i + 1. Lemma 5.7.10 zeigt dann, dass C[li ] ebenfalls
nach i Normalordnungsschritten zu einer WHNF wird. D.h. C[li ]⇓.
Damit ist das Kriterium von Lemma 5.7.4 erfüllt und wir können schließen, dass
l = clubi (li ) ist.
2
Beispiel 5.7.19 Eine Ungleichung ist i.a. nicht zulässig:
Die Ungleichheit 6= ist nicht stetig: Sei l = repeat 1 und li = partlist i l.
Dann gilt li 6∼c l für alle i, aber clubi (li ) = l. Wäre 6= stetig, dann ergäbe sich
l 6∼c l, was Unsinn ist.
Das Induktionsschema für unendliche Listen funktioniert für das Prädikat
P (x) ≡ x 6= l. Offenbar gilt bot 6∼c l und [] 6∼c l. Nimmt man xs 6∼c l als
Induktionshypothese und will man x : xs 6∼c l zeigen, so gibt es zwei Fälle.
Wenn x 6∼c 1, dann ist das offenbar richtig. Wenn x ∼c 1, dann erfordert
1 : xs 6∼c l wegen l = 1 : l gerade xs 6∼c l. Das war die Induktionsbehauptung.
Begründung des Induktionsschemas Stetigkeit und club werden benutzt.
Da alle Listen sich als club von partiellen Listen darstellen lassen, kann man
für jede Liste l eine aufsteigende Folge li mittels partlist definieren, die diese
Liste als club hat.
• Ist die Liste endlich, dann zeigt das Induktionsschema die Aussage direkt
mit normaler Induktion
• Ist die Liste unendlich, dann zeigt das Induktionsschema die Aussage P (l)
für alle partiellen Listen li = partlist l mit normaler Induktion. Da das
Prädikat zulässig ist, darf man wegen l = clubi (li ) schließen, dass P (l)
gilt.
Bemerkung 5.7.20 Wir wiederholen nochmal die Definitionen einiger Funktionen für das folgende:
take n xs
= case n of 0 ->
case xs of
append xs ys = case xs of {[]
length xs
= case xs of {[]
concat xs
= case xs of {[]
[], n+1 ->
[] -> [];(y:ys)-> y: (take n ys)
-> ys; u:us -> u:(append us ys)
-> 0 ; y:ys -> 1 + (length ys)}
-> []; u:us -> append u (concat us)}
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
52
reverse xs
= case xs of {[] -> []; u:us -> append (reverse us) (u:[])}
foldl f e xs = case xs of {[] -> e ; u:us -> foldl f (f e u) us}
foldr f e xs = case xs of {[] -> e ; u:us -> f u (foldr f e us)}
Aussage 5.7.21 (Take-Lemma) Gegeben zwei Listenausdrücke s, t. Dann
gilt s ∼c t gdw. für alle n > 0: take n s ∼c take n t
Beweis. Der Beweis kann leicht mittels Bisimulation geführt werden: Es folgt aus
der Voraussetzung, dass die beiden Listen äquivalente Elemente haben, sofern
die Elemente erreichbar sind. Es gilt auch, dass (drop n s)⇓ ⇔ (drop n t)⇓. 2
Vorsicht: Man kann aus s!!n ∼c t!!n für alle n ≥ 0 nicht schließen, dass s ∼c t
ist, da z.B. ⊥ 6∼c [⊥].
Beispiel 5.7.22 Zeige append [] xs ∼c xs.
Da append mit Fallunterscheidung über das erste Argument definiert ist, kann
man das sofort zu xs reduzieren.
Beispiel 5.7.23 Zeige append xs [] ∼c xs. Da append mit Fallunterscheidung über das erste Argument definiert ist, ist das nicht direkt mit Reduktion
zu zeigen. Wir benutzen das Induktionsschema für das erste Argument.
• append bot [] ∼c bot, da die Auswertung von bot durch die caseReduktion nicht terminiert.
• append [] [] ∼c [] mittels Reduktion
• append (x:xs) [] reduziert zu x: (append xs []). Nach Induktionshypothese ist (append xs []) ∼c xs. Da ∼c sich über Konstruktoren ziehen
lässt, erhalten wir append (x:xs) [] ∼c x:xs.
Insgesamt erhalten wir jetzt mit dem Induktionsschema die Behauptung.
Hiermit können wir jetzt weitere Gleichheiten zeigen:
Beispiel 5.7.24 append
ist
assoziativ.
Es
gilt:
(append (append bot xs) ys)= bot = (append bot (append xs ys))
Es
gilt:
(append (append [] xs) ys)=
(append xs ys)
=
(append [] append(xs ys))
Es gilt: (append (append (z:zs) xs) ys)= (append (z : append zs xs) ys)
= z : (append (append zs xs) ys)
= z : (append zs (append xs ys)) (mit Induktionshypothese)
= (append (z:zs) (append xs ys)).
Danach können wir das Induktionsschema verwenden.
Definition 5.7.25 Ein Induktionsschema zum Nachweis von Eigenschaften
P endlicher Listen, wobei P nicht stetig zu sein braucht.
• Zeige P ([])
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
53
• Zeige P (xs) ⇒ P (x : xs).
Dann kann man P (xs) für alle endlichen Listen xs schließen.
Definition 5.7.26 Ein weiteres Induktionsschema zum Nachweis von (beliebigen) Eigenschaften P endlicher Listen kann man auf der Länge der Listen
aufbauen.
• Zeige: P ([])
• Zeige:
P (xs) für alle Listen der Länge n
⇒ P (ys) für alle Listen der Länge n + 1
Dann kann man P (xs) für alle endlichen Listen schließen.
Beispiel 5.7.27 Wir vergleichen drei Längenfunktionen:
length1 xs = case xs of [] -> 0; y:ys -> 1+(length ys)
length2 xs = foldr (\x _ -> 1+x) 0 xs
length3 xs = foldl’ (\x _ -> 1+x) 0 xs
Behauptung: die ersten beiden Definition sind gleich:
length1 bot
= bot
length2 bot
= foldr (\x _ -> 1+x) 0 bot = bot
length1 []
= 0
length2 []
= foldr (\x _ -> 1+x) 0 [] = 0
length1 (u:us) = 1+(length us)
length2 (u:us) = foldr (\x _ -> 1+x) 0 (u:us)
= 1 + (foldr (\x _ -> 1+x) 0 us
Behauptung: die dritte ist ebenfalls gleich der ersten:
length3 bot
= bot
length3 []
= 0
length3 (u:us) = foldl’ (\x _ -> 1+x) 0 (u:us)
= strict (foldl (\x _ -> 1+x)) (1 + 0) us
= strict (foldl (\x _ -> 1+x)) 1 us
Hier braucht man einen Gleichheitsbeweis mit Induktion nach der Länge der
Listen für endliche Listen und eine extra Argumentation, dass sich bot für unendliche Listen ergibt.
Beispiel 5.7.28 Für alle endlichen Listen gilt:
• length(xs ++ ys) = (length xs) + (length ys)
Das zeigt man mit Induktion nach der Länge der Listen:
Wenn xs = [], dann gilt die Behauptung.
length((x:xs) ++ ys) reduziert zu length(x:(xs ++ ys); dies reduziert zu 1+ length(xs ++ ys). Mit Induktion ist das gleich
1+ length(xs) + length ys). Da length((x:xs) zu 1 + length xs
reduziert und + assoziativ ist, erhält man die Behauptung.
54
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
• length (reverse xs)= length xs
Für Listen der Länge 0 gilt die Behauptung.
Für Listen der Länge > 0 erhält man:
length (reverse (x:xs))→ length (reverse xs) ++ [x])
Mit obiger Gleichung erhält man: = length (reverse xs) + 1)
Induktion nach der Länge ergibt: = (length xs) + 1) = (length x:xs)
• reverse (reverse xs) = xs.
Das ist komplizierter: Man benötigt als Zwischenbehauptungen: ++ ist assoziativ und reverse(xs ++ ys) = (reverse ys) ++ (reverse xs). Die
sind jeweils mit Induktion zu zeigen.
Danach kann man für nichtleere Listen schließen:
reverse (reverse (x:xs)
= reverse ((reverse xs) ++ [x])
= reverse [x] ++ (reverse(reverse xs))
= [x] ++ xs = x:xs.
Aber für unendliche Listen ist die letzte Aussage
reverse (repeat 1) = bot und reverse bot = bot.
falsch,
denn
Bemerkung 5.7.29 P (x) ≡ reverse (reverse xs) = xs ist ein Beispiel
für ein stetiges (zulässiges) Prädikat, das sowohl für alle endlichen Listen
als auch für xs = bot gilt, aber nicht für alle Listen. Der Beweis, dass
reverse (reverse xs) = xs für alle endlichen Listen gilt wurde nicht nach
dem Schema für alle Listen, sondern nach der Länge der Listen geführt.
Induktionsschema für alle Ausdrücke: Eine Erweiterung der Induktion
kann zum Nachweis von stetigen Eigenschaften P (·) benutzt werden, wobei alle
Ausdrücke als Argumente zulässig sind. Insbesondere kann dieses Schema zum
Nachweis von Gleichungen benutzt werden.
• Zeige P (⊥).
• Zeige P (λx.r).
• Zeige für alle Konstruktoren c: P (x1 )∧. . .∧P (xn ) ⇒ P (c x1 . . . xn )) wobei
n = ar(c).
Dann kann man schließen: P (s) gilt für alle Ausdrücke s.
Bemerkung 5.7.30 Das Schema scheint nicht die Verallgemeinerung des Induktionsschemas für alle Listen zu sein. Aber es ist doch konsistent dazu, denn
der Induktionsschritt P (x) ∧ P (xs) ⇒ P (x : xs) passt trotzdem, denn im Falle der Listen zeigt man den Schritt für alle x. Im Falle der Betrachtung aller
Ausdrücke zeigt man ja gerade P (x) für alle Ausdrücke x.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
55
Beispiel 5.7.31 Am Beispiel der Assoziativität von append kann man zeigen,
dass die Assoziativitätsgleichung
append(append x y) z = append(x (append y z))
für alle Ausdrücke x, y, z gilt.
• Für x = (λx.r) erhält man auf beiden Seiten ∼c bot, da ein Typfehler
auftritt.
• Für x = c x1 . . . xn erhält man ebenfalls auf beiden Seiten ∼c bot, da ein
Typfehler auftritt.
• die anderen Fälle wurden bereits gezeigt.
Bemerkung 5.7.32 Diese Gleichheiten kann man verwenden, um Programme
in korrekter Weise zu verändern. Z.B. darf man die Assoziativität von append
verwenden, um beliebige append-Ausdrücke umzuklammern. Dies wäre falsch,
wenn die Assoziativität nur für endliche Listen gelten würde.
Wenn klar ist, dass an einer Stelle im Programm nur endliche Listen verwendet
werden können, darf man dort auch die Gleichheiten verwenden, die für endliche
Listen gelten. Z.B. wenn nach der Abfrage (liste-endlich l) die Variable l
verwendet wird, dann kann man das wie eine endliche Liste behandeln, denn im
Falle einer unendlichen Liste ergibt sich bot bereits bei der Abfrage.
Übungsaufgabe 5.7.33 Welche der folgenden Gleichungen gelten für endliche, welche für unendliche Listen?
• xs ++ [] = xs?
• concat (xs ++ ys) = (concat xs) ++ (concat ys)?
• (take n xs) ++ (drop n xs) = xs?
• Für Listen xs, ys gleicher Länge: unzip (zip xs ys) = (xs,ys)? mit
den zwei Definitionsvarianten
unzip_1
unzip_2
= foldr (\(a,b) paar ->
(a:(fst paar), b:(snd paar))) ([], [])
= foldr (\(a,b) (as,bs) -> (a:as, b:bs)) ([], [])
• map f (map g xs) = map (f.g) xs (Verschmelzungsgesetz für map)
5.7.2
Gleichungen für (un-)endliche Listen: map, concat,
foldr, foldl
Das Buch von Richard Bird, 98 enthält einige Gleichungen und Gesetze zum
Zusammenhang zwischen foldr, foldl, map, concat und weiteren Funktionen. Diese Gleichungen können für verschiedene Nachweise der Korrektheit und
für Optimierungen verwendet werden. Im folgenden einige dieser Gleichungen:
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
56
Diese praktische Verwendung dieser Gleichungen liegt in der Optimierung zur
Compilezeit, wobei man diese Gleichungen als Programmtransformationen verwendet. Allerdings gibt es viele Gleichungen, die man alle einzeln von Hand
verifizieren müsste, bevor man sie in einen Compiler einbaut. Die automatische
Generierung ist zT möglich, aber auch sehr aufwändig. Als weitere Information
über die Verwendbarkeit der Gleichungen braucht man noch Umgebungsinformation: Z.B.: ist die lokale Funktion f strikt? Ist ein Ausdruck t immer eine
endliche Liste? Was man natürlich auch noch wissen muss, ist ob eine Regel gut
ist, d.h. wirklich etwas verbessert.
Global muss man wissen, ob die Anwendung der Transformationen im Compiler terminiert, und ob die Regeln die Tendenz haben, das Programm (evtl.
überproportional) zu vergrößern.
Sei f die binäre Verknüpfung eines Monoids mit dem Einselement e. D.h. als
Funktion sei f eine zweistellige Funktion, e ein Ausdruck, so dass modulo ∼c
die Monoidgleichungen gelten:
f x (f y z) = f (f x y) z Assoziativität
f xe
= x
Einselement
f ex
= x
Einselement
Wenn man nur ein Halbgruppe hat, kann es auch ein Nullelement geben; ⊥
wirkt bei strikten Operatoren wie ein Nullelement.
f bot x = bot Nullelement
f x bot = bot Nullelement
Erstes Dualitätsgesetz für FOLD
Für alle endlichen Listen xs mit Elementen aus dem Monoid oben gilt:
foldr f e xs = foldl f e xs
Begründung Die Definitionen sind:
foldr
:: (a -> b -> b) -> b -> [a] -> b
foldr f z []
= z
foldr f z (x:xs) = f x (foldr f z xs)
foldl
:: (a -> b -> a) -> a -> [b] -> a
foldl f z []
= z
foldl f z (x:xs) = foldl f (f z x) xs
Wir verwenden die Übersetzung, die zuerst ein case auf die Liste macht (s.o.)
Zuerst muss man mit Induktion nachweisen, dass man im Monoid bleibt,
d.h. die Ergebnisse des fold und foldr sind wieder im Monoid. Danach die
Gleichung
Mit Induktionsschema []; x:xs
Für die leere Liste gilt: foldr f e [] =
e = foldl f e []
57
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Für eine Liste [x] gilt: foldr f e [x] = f x e = x
Für eine Liste x1:x2:xs gilt: foldr f e (x1:x2:xs)
Assoziativität
des
Operators
f
ergibt:
f (f x1 x2) (foldr f e xs)) = foldr f e (f x1
Dieselbe Rechnung für foldl ergibt:
foldl f e (x1:x2:xs) = foldl f (f (f e x1)
ergibt foldl f (f e (f x1 x2)) xs = foldl f
kann man Induktion über die Länge machen.
= f e x = foldl f e [x]
= f x1 (f x2 (foldr f e xs)).
das
ist
gleich
x2) : xs))
x2) xs. Assoziativität
e (f x1 x2):xs. Jetzt
Bemerkung 5.7.34 Offenbar gilt, dass die Gleichung für unendliche Listen
falsch sein kann, denn foldl ergibt immer bot für unendliche Listen, während
foldr auch Ergebnisse liefern kann. Der bot-Fall gilt, aber die versteckte Annahme in der Induktion ist der Fall [x], der gilt, aber man muss noch den Fall
x:bot untersuchen. In diesem Falle erhält man: einerseits f x bot, andererseits bot.
D.h. das erste Dualitätsgesetz für fold gilt für alle Listen, wenn der Operator
f strikt im zweiten Argument ist: wenn f x bot = bot für alle x. Im Fall unendlicher Listen sieht man schnell, dass dann das Ergebnis jeweils ⊥ ist, und
deshalb gleich.
Zweites Dualitätsgesetz für FOLD
Seien f,g Operatoren, e eine Einheit, so dass
• x 0 f0 (y 0 g0 z) = (x 0 f0 y) 0 g0 z
• x 0 f0 e = e 0 g0 x
Dann gilt für alle endlichen Listen xs:
foldr f e xs = foldl g e xs
Zunächst muss man folgende Zwischenbehauptung für alle endlichen Listen xs
zeigen:
x ’f’ (foldl g y xs) = foldl g (x ’f’ y) xs
Beweis:
Fall []: Beide Seiten reduzieren auf x ’f’ y
Fall (z:zs): x ’f’ (foldl g y (z:zs)) = x ’f’ (foldl g (g y z) zs)
= (Induktion) foldl g (x ’f’ (y ’g’ z)) zs)
= (Voraussetzung) foldl g ((x ’f’ y) ’g’ z) zs)
Für die andere Seite erhält man:
foldl g (x ’f’ y) (z:zs) = foldl g ((x ’f’ y) ’g’ z) zs
Die Zwischenbehauptung ist somit gezeigt.
Nachweis der eigentlichen Behauptung mit Induktion:
• Fall []: foldr f e [] = e = foldl g e []
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
58
• Fall x:xs: foldr f e (x:xs) = f x (foldr f e xs)
= (Induktion) f x (foldl g e xs)
Die
Zwischenbehauptung
zeigt
jetzt
mit
y
=
e
die
Gleichung
f x (foldl g e xs)
=
foldl g (f x e) xs) = foldl g (g e x) xs).
Andererseits:
foldl g e (x:xs) = foldl g (g e x) xs und damit gilt die Behauptung des Dualitätssatzes.
Beispiel 5.7.35
Mit dem zweiten Dualitätssatz für fold kann man die Gleichheit der beiden reverse-Definition zeigen.
• reverse1 = foldr snoc [] where snoc x xs = xs ++ [x]
Das entspricht der Definition
reverse1 [] = []
reverse1 (x:xs) = (reverse1 xs) ++ [x]
was man leicht durch Induktion nachweisen kann (für alle Listen)
• reverse2 = foldl cons [] where cons xs x = x:xs
Das entspricht der Definition
reverse2 xs
= reverse2r xs []
reverse2r []
ys = ys
reverse2r (x:xs) ys = reverse2r xs (x:ys)
Um zu zeigen, dass die Ergebnisse gleich sind für endliche Listen, muss man nur
nachweisen, dass x ’snoc’ (y ’cons’ z) = (x ’snoc’ y) ’cons’ z D.h.
dass (z:y) ++ [x] = z:(y ++ [x])]. Das kann man mit einem Reduktionsschritt von append zeigen.
Man hat noch nachzuweisen, dass auch x ’snoc’ [] = [] ’cons’ x. Beides
reduziert zu [x].
Drittes Dualitätsgesetz für FOLD
Für alle endlichen Listen xs gilt:
foldr f e xs = foldl (flip f ) e (reverse xs)
Für alle Listen xs gilt:
foldl f e xs = foldr (flip f ) e (reverse xs)
Die Gleichungen sind konsistent mit der Intuition:
foldr + 0 [1,2,3,4]
ergibt
1 + (2 + (3 + (4 + 0))),
während
foldl (flip +) 0 [4,3,2,1] ergibt (((0 +’ 4) +’ 3) +’ 2) +’ 1 mit
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
59
flip + = +’.
Nachweis
Zunächst eine andere Gleichung, die für alle Listen gilt, den Beweis kann man
als Übungsaufgabe durchführen.
foldr g e (xs++ys) = foldr g (foldr g e ys) xs
Jetzt der Nachweis der zweiten Gleichung:
• Fall ungetypt. D.h. xs ist Abstraktion oder hat falschen Topkonstruktor:
Beide Seiten sind äquivalent zu bot.
• Fall bot: Beide Seiten reduzieren zu bot.
• Fall []: Beide Seiten reduzieren zu e.
• Fall (x:xs).
foldl f e (x:xs) → foldl f (f e x) xs
foldr (flip f) e (reverse (x:xs))
→ foldr (flip f) e ((reverse xs) ++ [x]))
= (ZWbeh) foldr (flip f) (foldr (flip f) e [x]) (reverse xs)
= foldr (flip f) (f e x) (reverse xs)
= (Induktion) foldl f (f e x) xs
Damit sind die beiden Seiten gleich, und die Behauptung gilt für alle
Listen.
Übungsaufgabe 5.7.36 Ist foldr (:) [] = Id ?
Beispiel 5.7.37 Den ersten Teil des Dualitätssatzes kann man verwenden, um
reverse (reverse xs) = xs für endliche Listen zu zeigen:
foldr (:) [] xs = foldl (flip (:)) [] (reverse xs)
Die linke Seite wird zu xs, da xs endliche Liste. Auf der rechten Seite ist foldl (flip (:)) [] = reverse, deshalb ist die rechte Seite gerade
reverse (reverse xs).
Übungsaufgabe 5.7.38 Warum kann man mit der zweiten Aussage des Dualitätssatzes nicht reverse (reverse xs) = xs für alle (auch unendliche Listen) zeigen?
Verschmelzungs-Gesetze für Fold
Der Sinn und Zweck dieser Gesetze ist die Optimierung der Listenverarbeitung,
die Zwischenlisten erzeugt und gleich wieder verwirft. Diese Zwischenlisten
sollen vermieden werden durch automatische Umstellung der Verarbeitung.
FOLD-MAP Verschmelzung
60
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
foldr f a . map g = foldr (f.g) a
Das ist einfach nachzuweisen:
• Fall bot: ergibt bot.
• Fall []: ergibt a.
• Fall x:xs:
Linke Seite: foldr f a . map g (x:xs)
= foldr f a ((g x) : map g xs) = f (g x) (foldr f a (map g xs))
Die rechte Seite ergibt:
foldr (f.g) a (x:xs) = f (g x) (foldr (f.g) a xs).
Anwenden der Induktionshypothese zeigt die Behauptung für den Fall
x:xs.
• Danach kann man das Induktionsschema für Listen anwenden.
Die Optimierung besteht darin, die Verwendung von map zu eliminieren, denn
die Ergebnisliste wird von map aufgebaut und von foldr gleich wieder abgebaut.
Das Verschmelzungsgesetz für foldl und map ist:
foldl f a . map g
=
foldl (\x,y -> f x (g y))
a
Beispiel 5.7.39 Zu foldl-map-Verschmelzung. Das Ergebnis bei Anwendung
auf eine Liste [x1,x2,x3] ist gerade ((a + (g x1)) + (g x2)) + (g x3)
Nachweis
• Fall bot: Das Ergebnis ist jeweils bot.
• Fall []: Das Ergebnis ist jeweils [].
• Fall x:xs
foldl f a . map g (x:xs)
= foldl f a ((g x): map g (x:xs))
= foldl f (f a (g x)) (map g xs)
Auf der rechten Seite ergibt sich: (foldl (\x,y -> f x (g y)) a (x:xs)
= foldl (f.g) (f a (g x)) xs.
Anwenden der Induktionshypothese liefert die Behauptung in diesem
Fall.
• Danach kann man das Induktionsschema für Listen anwenden.
FOLD-CONCAT Verschmelzung
foldr f a . concat = foldr (flip (foldr f)) a
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
61
Zum Nachweis dieser Gleichung benutzt man das Induktionsschema für Listen:
Die Fälle bot, [] sind einfach. Der Nachweis für (xs:xss) hat drei Unterfälle
Fall []:xss:
foldr f a (concat ([]:xss)) = foldr f a (concat (xss))
foldr (flip (foldr f)) a ([]:xss))
= ((flip (foldr f)) [] (foldr (flip (foldr f)) a (xss)))
= foldr f (foldr (flip (foldr f)) a xss) []
= foldr (flip (foldr f)) a xss
Fall bot:xss:
(foldr f a . concat) (bot:xss)
= foldr f a (concat (bot:xss))
= foldr f a (foldr (++) [] (bot:xss))
= foldr f a (bot ++ foldr (++) [] (xss))
= foldr f a bot
= bot
foldr (flip (foldr f)) a (bot:xs)
= (flip (foldr f)) bot (foldr (flip (foldr f)) a xs)
= foldr f (foldr (flip (foldr f)) a xs) bot
= bot
Fall (x:xs):xss:
foldr f a (concat (x:xs):xss) = foldr f a (x: concat (xs:xss))
= f x (foldr f a (concat (xs:xss)))
foldr (flip (foldr f)) a ((x:xs):xss)
= (flip (foldr f)) (x:xs) (foldr (flip (foldr f)) a xss)
= foldr f (foldr (flip (foldr f)) a xss) (x:xs)
Dazu zunächst noch die folgende Rechnung:
= f x (foldr f (foldr (flip (foldr f)) a xss) xs)
foldr (flip (foldr f)) a (xs:xss)
= (flip (foldr f)) xs (foldr (flip (foldr f)) a xss)
= (foldr f (foldr (flip (foldr f)) a xss) xs)
Insgesamt hat man das Gesetz nachgewiesen.
Übungsaufgabe 5.7.40 Wie sieht das Gesetz für foldl aus?
foldl f a . concat = ???
FOLD-FILTER Verschmelzung
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
62
foldr f a (filter p xs) = foldr (\x y -> if (p x) then f x y else y) a xs
foldl f a (filter p xs) = foldl (\x y -> if (p y) then f x y else x) a xs
Den Nachweis kann man analog wie für die anderen Regeln führen.
Fold auf Bäumen: Verschmelzungsgesetze
Die Faltung auf binären Bäumen kann man wie folgt definieren:
data Binbaum a = Blatt
a | Knoten (Binbaum a) (Binbaum a)
-- foldr Entsprechung
kann terminieren f"ur unendliche B"aume
foldrbt :: (a -> b -> b) -> b -> Binbaum a -> b
foldrbt op a (Blatt x) = op x a
foldrbt op a (Knoten x y) = (foldrbt op (foldrbt op a y) x)
-- foldl Entsprechung
terminiert nie f"ur unendliche B"aume
foldlbt :: (a -> b -> a) -> a -> Binbaum b -> a
foldlbt op a (Blatt x) = op a x
foldlbt op a (Knoten x y) = (foldlbt op (foldlbt op a x) y)
-- entspricht foldl mit Platzoptimierung:
foldlbtstr op a (Blatt x) = op a x
foldlbtstr op a (Knoten x y) = (strict (foldlbtstr op) (foldlbtstr op a x) y)
-- entspricht map:
mapbt f (Blatt x) = Blatt (f x)
mapbt f (Knoten bl br) =
Knoten (mapbt f bl)
(mapbt f br)
-- reverse-Entsprechung
reversebt (Blatt x) = (Blatt x)
reversebt (Knoten x y) = Knoten (reversebt y) (reversebt x)
Ein Verschmelzungsgesetz für foldrbt ist die Vermeidung des Baumaufbaus
nach dem map über den Baum:
foldrbt op a . mapbt f
= foldrbt (\x y -> (op (f x) y)) a
Der Nachweis des Gesetzes für alle binären Bäume, wobei man ein auf binäre
Bäume angepasstes Induktionsschema verwenden muss.
• Für bot ist das Ergebnis jeweils bot.
• Für Blatt x ist das Ergebnis jeweils op (f x) a.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
63
• Für Knoten x y ergibt sich links: foldrbt op a (mapbt f (Knoten x y))
→ foldrbt op a (Knoten (mapbt f x) (map f y))
foldrbt op (foldrbt op a (mapbt f y)) (mapbt f x)
Es ergibt sich rechts:
foldrbt (\x y -> (op (f x) y)) a (Knoten x y)
→ foldrbt (\x y -> (op (f x) y)) (foldrbt (\x y -> (op (f x) y)) a y)
Jetzt kann man die Induktionshypothese anwenden.
x
Das Verschmelzungsgesetz für foldlbt ist:
foldlbt op a . mapbt f
= foldlbt (\x y -> (op x (f y))) a
Bei Kombinationen zwischen Listen und Bäumen kann man ebenfalls solche
Verschmelzungen durchführen.
randbt = foldbt (:) []
Eine Verschmelzungsregel ist:
map f . randbt = randbt (\x,y -> (f x):y)
Der Beweis sei als Übungsaufgabe dem Leser überlassen.
Bäume mit beliebig vielen Töchterknoten
Im folgenden der Quellcode mit Testfunktionen:
data Gbaum a = Gblatt
a | Gknoten [Gbaum a]
-- foldr Entsprechung
kann terminieren f"ur unendliche B"aume
foldrgt :: (a -> b -> b) -> b -> Gbaum a -> b
foldrgt op a (Gblatt x) = op x a
foldrgt op a (Gknoten []) = a
foldrgt op a (Gknoten (x:xs)) = (foldrgt op (foldrgt op a (Gknoten xs)) x)
testrgt = foldrgt (:) [] (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testrgt2 = foldrgt (:) [] (Gknoten (map Gblatt [1..]))
-- foldl Entsprechung
kann terminieren f"ur unendliche B"aume
foldlgt :: (a -> b -> a) -> a -> Gbaum b -> a
foldlgt op a (Gblatt x) = op a x
foldlgt op a (Gknoten []) = a
foldlgt op a (Gknoten (x:xs)) = (foldlgt op (foldlgt op a x) (Gknoten xs))
testlgt = foldlgt (flip (:)) [] (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testlgt2 = foldlgt (flip (:)) [] (Gknoten (map Gblatt [1..]))
-- testlgt2 = bot
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
64
-- entspricht foldl mit Platzoptimierung:
foldlgtstr op a (Gblatt x) = op a x
foldlgtstr op a (Gknoten []) = a
foldlgtstr op a (Gknoten (x:xs)) =
(strict (foldlgtstr op) (foldlgtstr op a x) (Gknoten xs))
testlgtstr = foldlgtstr (flip (:)) []
(Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
testlgtstr2 = foldlgtstr (flip (:)) []
(Gknoten (map Gblatt (take 10000 [1..])))
mapgt f (Gblatt a) = Gblatt (f a)
mapgt f (Gknoten (xs)) = Gknoten (map (mapgt f) xs)
testmapgt = mapgt quadrat (Gknoten [Gknoten [Gblatt 1, Gblatt 2], Gblatt 3])
-- Reverse
reversegt (Gblatt x) = (Gblatt x)
reversegt (Gknoten b) = Gknoten (foldl (\x y -> (reversegt y) : x) [] b)
Die Verallgemeinerung des fold-map Verschmelzungsgesetzes ist:
foldrgt op a . mapgt f
= foldrgt (\x y -> (op (f x) y)) a
Dies ergibt für Gblatt a gerade Gblatt (f a).
Für (Gknoten []): Beide Seiten ergeben a.
Für (Gknoten (x:xs)):
foldrgt op a (mapgt f (Gknoten (x:xs)))
→
foldrgt op a (Gknoten (map (mapgt f) (x:xs)))
→
foldrgt op a (Gknoten ((mapgt f x) : map (mapgt f) xs))
→
(foldrgt op (foldrgt op a (Gknoten (map (mapgt f) xs))) (mapgt f x)).
Die rechte Seite ergibt: foldrgt (\x y -> (op (f x) y)) a (Gknoten (x:xs))
→
foldrgt (\x y -> (op (f x) y)) a (Gknoten (x:xs))
→
foldrgt (\x y -> (op (f x) y))
(foldrgt (\x y -> (op (f x) y)) a (Gknoten xs))
Jetzt kann man ein für Gbaum angepasstes, verallgemeinertes Induktionsschema
verwenden.
5.7.3
Nachweis von Aussagen in typisierten funktionalen
Programmiersprachen
In diesem Paragraphen nehmen wir an, dass alle Ausdrücke und Unterausdrücke
polymorph getypt sind. Die Grundausdrücke vom Grundtyp2 α bezeichnen wir
mit Exp(α). Wir nehmen an, dass die Typisierung die folgenden Eigenschaften
erfüllt:
2 ein
Grundtyp ist ein Typ ohne Variablen
x
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
65
• Wenn t ein geschlossener Ausdruck ist, α ein Grundtyp, t :: α, und t → t0 ,
dann auch t0 :: α.
• Wenn t :: τ = (c α1 . . . αn ), wobei c ein Typkonstruktor ist, aber nicht
gleich →, dann gilt einer der Fälle:
– t hat keine WHNF, oder
– t hat eine WHNF der Form ci t1 . . . tm , und ci ist ein Datenkonstruktor zum Typ c, und der Typ von tj ist eindeutig bestimmbar. D.h. es
gibt eine Funktion comptyp(τ, ci , j), die einen Grundtyp liefert, und
es gilt tj :: comptyp(τ, ci , j).
• Wenn t geschlossen ist, und t :: α1 → α2 , dann hat t entweder keine
WHNF oder eine FWHNF.
Jetzt können wir das Induktionsschema angeben:
Induktionsschema für stetige Prädikate unter Typisierung:
• Zeige P (⊥).
• Wenn α = τ1 → τ2 ein Funktionstyp, dann zeige P (λx : τ1 .r :: τ2 ).
• Wenn α = (c τ1 . . . τn ): Zeige für alle Konstruktoren cj , die zum Typ c
gehören:
P (x1 :: comptyp(α, cj , 1)) ∧ . . . ∧ P (x1 :: comptyp(α, cj , ar(cj ))
⇒ P (cj x1 . . . xar(cj ) ))
Dann kann man schließen: P (s) gilt für alle Typen α und alle Ausdrücke s : α.
Die Begründung für dieses Induktionsschema ist analog wie für unendliche Listen bzw. für alle Ausdrücke: Für Ausdrücke, die aus endlich vielen Konstruktoren, bot und FWHNFs aufgebaut sind, kann man die Eigenschaft dann mit
Induktion begründen. Für die Ausdrücke, die man nur “unendlich“ darstellen
kann mittels Konstruktoren, bot und FWHNFs, verwendet man Stetigkeit. Die
Aussage für alle Ausdrücke erhält man über die clubs von Partialausdrücken
analog zu Partiallisten. Der entsprechende Beweis ist analog zu Lemma 5.7.18.
5.8
Änderung
strikt
der
Auswertungsreihenfolge:
Die gezielte Änderung der Auswertungsreihenfolge ist eine Optimierung, die von
allen Kompilern für nicht-strikte FPS verwendet wird. Auch hier ist wesentlich,
dass die Semantik, sprich die kontextuelle Äquivalenz, erhalten bleibt.
Beispiel 5.8.1 Betrachte nochmal die Funktionen
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
NT x
K x y
=
=
66
NT x
x
Ändert man die Auswertungsreihenfolge so ab, dass die Funktion K zunächst das
erste, dann das zweite Argument auswertet und dann das Resultat berechnet,
dann terminiert die Auswertung von K 1 bot nicht mehr, obwohl (K 1 bot)⇓.
D.h. offenbar ist die Semantik verändert.
Ändert man die Auswertungsreihenfolge so ab, dass der Superkombinator K bei
jeder Auswertung zunächst das erste Argument auswertet und dann das Resultat
berechnet, dann ist der Fehler behoben. Eine erste Vermutung ist, dass diese
Änderung richtig ist, aber der Nachweis fehlt.
Im folgenden werden wir den Nachweis erbringen, dass diese Veränderung die
Semantik erhält.
Zuerst brauchen wir noch eine Betrachtung zur Normalordnungsreduktion.
Lemma 5.8.2 Wenn ein geschlossener Ausdruck t eine Normalordnungsredukno,∗
tion t −−−→ R[s1 ] hat, wobei R ein Reduktionskontext ist, dann wird die Normalordnungsreduktion den Unterausdruck s1 zunächst solange reduzieren, bis er
selbst in WHNF ist. Es kann folgendes passieren:
no,∗
no
no,∗
no
no
no
• t −−−→ R[s1 ] −→ R[s2 ] −→ . . . −→ R[sn ], wobei sn in WHNF ist, und
danach wird evtl. auch R durch die Normalordnungsreduktion verändert
no
• t −−−→ R[s1 ] −→ R[s2 ] −→ . . . und der Ausdruck s1 wird niemals zur
WHNF reduziert.
Zunächst definieren wir Striktheit.
Definition 5.8.3 Ein KFP-Ausdruck f ist strikt, gdw. (f bot)⇑. Hierbei ist bot
ein nichtterminierender Ausdruck.
Eine Erweiterung ist die Definition der Striktheit in einem Argument:
Definition 5.8.4 Die (geschlossene) n-stellige Funktion f ist strikt im i-ten
Argument, gdw. für alle geschlossenen Ausdrücke tj : (f t1 . . . ti−1 bot ti . . . tn ).
Man könnte auch eine (i, n)-Striktheit definieren, wenn man die Sprache mit Superkombinatoren betrachtet: f ist (i, n)-strikt gdw. für alle geschlossenen Ausdrücke tj : (f t1 . . . ti−1 bot ti+1 . . . tn )⇑.
Definition 5.8.5 Sei f strikt. Dann sei die f -strikte Auswertung diejenige, die
bei Auswertung von t, falls ein Unterterms f s auszuwerten ist, zuerst s mit fstrikter Auswertung in WHNF überführt, und ansonsten wie die Normalordnung
reduziert.
Lemma 5.8.6 Wenn f strikt ist, und (f s)⇓, dann gilt s⇓, und die Anzahl der
Normalordnungsreduktionen von s ist echt kleiner als die Anzahl der Normalordnungsreduktionen von f s
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
67
Beweis. Es ist klar, dass s⇓. Was noch fehlt, ist die Aussage zur Anzahl. Markiert
man den Term s und alle während der Reduktion entstehenden Kopien, dann
gibt es zwei Fälle:
1. Die Normalordnungs-Reduktion von f s reduziert einen der markierten Unterterme s während der Reduktion ebenfalls als Subprozedur zur
WHNF. Dann gilt die Behauptung.
2. Die Normalordnungs-Reduktion reduziert niemals die markierten Kopien
von s. Dann kann man aber s durch bot ersetzen und die NormalordnungsReduktion terminiert immer noch. Das ist ein Widerspruch zur Annahme
der Striktheit von f .
2
Beispiel 5.8.7 Beachte: es gilt nicht dass aus s⇓ , f strikt auch folgt, dass
(f s)⇓: Sei f x = bot Dann ist f strikt, und sei s = True. Dies ist eine
WHNF. Damit terminiert (f s) nicht, obwohl s terminiert.
Definition 5.8.8 Eine Auswertungsstrategie S ist eine Relation t →S t0 auf
Termen. Diese Relation muss effektiv sein, d.h es muss einen Algorithmus geben,
der bei Eingabe des Terms t das Redukt t0 ausgibt, oder sagt: keine Reduktion
möglich, oder: ist eine WHNF.
∗
Eine Strategie S ist korrekt, gdw. für alle Terme t: t⇓ ⇔ t −
→S t0 , wobei t0 eine
WHNF ist.
Satz 5.8.9 Die f-strikte Auswertung ist eine korrekte Strategie
Beweis. Die eine Richtung ist klar, denn die f-strikte Auswertung ist mit Reduktionen simulierbar. Danach kann man den Satz 5.3.11 anwenden.
Jetzt zeigen wir: Wenn t⇓, dann terminiert auch die f-strikte Auswertung von t
mit einer WHNF. Angenommen, das ist falsch. Dann können wir einen Ausdruck
t finden, so dass die Normalordnung terminiert, aber nicht die f-strikte Auswertung. Wir wählen die Anzahl der Normalordnungsreduktionen von t minimal
und als zweiten Parameter die Größe von t minimal.
Falls der Normalordnungsredex in t nicht von der Form f s ist, stimmen beide
Reduktionen überein. Damit ist das Beispiel nicht minimal.
Auch wenn der Term bereits in WHNF ist, terminieren beide Reduktionen.
Also brauchen wir nur noch den Fall zu betrachten, dass ist der Normalordnungsredex von der Form f s ist und t = R[(f s)], wobei R ein Reduktionskontext ist.
Da f strikt ist, und (f s)⇓ gelten muss, gilt auch s⇓. Nach Lemma 5.8.6 ist die
Anzahl der Normalordnungsreduktionen von s kleiner als die von f s. Wegen
der Minimalität des Gegenbeispiels terminiert die f -strikte Reduktion von s.
Jetzt betrachten wir die f -strikte Reduktionsfolge R[f s] →S R[f s1 ] →S . . . →S
no
no
R[f sn ] −→ R[r[sn /x]] wobei f = λx . r. Es gilt wegen R[f s] −→ R[r[s/x]].
no
∗
Aus Aussage 5.3.5 und der Tatsache R[f s] −→ R[r[s/x]] −
→ R[r[sn /x]
können wir jetzt schließen, dass die Anzahl der Normalordnungsreduktionen
von R[r[sn /x]] echt kleiner als die von R[f s] ist, also gilt für diesen Ausdruck
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
68
die Behauptung. Damit gilt die Behauptung auch für den Ausdruck R[f s], da
wir eine terminierende f-strikte Reduktion durch Zusammensetzen konstruiert
haben.
2
Die Behauptung gilt auch, wenn wir statt einer einzigen Funktion f eine Menge
von Funktionen nehmen.
5.8.1
Andere Auswertungen: hyperstrikt
Die Kernsprache KFP kann die Auswertung sowohl für strikte als auch nichtstrikte FPS mit case und Konstruktoren modellieren.
Aber auch hier ist wieder eine Warnung angebracht: In einer strikten FPS sind
alle Abstraktionen und alle Konstruktoren strikt, (das case-Konstrukt nicht),
so dass man als Bild aller Ausdrücke nur eine Teilmenge von KFPTS hat, was
wieder eine veränderte kontextuelle Ordnung bewirkt:
Z.B. ist λf . bot =c λf . f bot in einer strikten FPS, da man nur strikte Objekte
für f einsetzen kann. Diese Funktionen sind in KFP aber unterscheidbar, indem
man sie auf λx . True anwendet.
Bemerkung 5.8.10 Den Operator hypereval, der Ausdrücke hyperstrikt evaluiert, kann man in KFP definieren: Die Wirkungsweise soll folgendermaßen
sein:
• hypereval(λx.r) → (λx.r) (Auswertung beendet)
no
• hypereval(t) → hypereval(t0 ), falls t −→ t0 (Auswertung)
• hypereval(c t1 . . . tar(c) ) → c (hypereval t1 ) . . . (hypereval tar(c) )
Die Definition dieses Operators kann man als Übungsaufgabe machen, man
braucht dazu eigentlich nur ein rekursives case.
Als Fazit können wir festhalten, dass
• Sowohl der Operator strict als auch hyperstrict in KFP definierbar
sind, und somit kein Problem darstellen.
• Die Gleichheiten, die in KFP für korrekt übersetzte Funktionen einer strikten FP gelten, gelten auch bzgl der kontextuellen Gleichheit der strikten
funktionalen Programmiersprache.
Aber: in strikten funktionalen Programmiersprachen wird teilweise argumentiert, dass man nicht-strikte Funktionen definieren kann, indem man
Auswertung verzögert durch abstrahieren mit lambda und Auswertung
erzwingt durch Anwendung, erhält man offenbar doch nicht die richtige
Gleichheit, wie man sieht. Diese Simulation ist nicht korrekt.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.8.2
69
Strikte Superkombinatoren
Definition 5.8.11 Eine Funktion (Superkombinator) f mit Definition
f x1 . . . xn = t heißt strikt in k-ten Argument, wenn folgendes gilt:
wenn ak , 1 ≤ k ≤ n keine WHNF hat, dann gilt für alle Ausdrücke
a1 , . . . , an : (f a1 . . . an ) hat keine WHNF.
Da man als semantischen Wert alle Ausdrücke ohne WHNF mit ⊥ zusammenfasst, schreibt man dies auch oft kürzer als
(f a1 . . . ak−1 ⊥ ak+1 . . . an ) = ⊥
Eine Funktion f mit Definition f x1 . . . xn = t heißt hyper-strikt in k-ten
Argument, wenn für alle Ausdrücke a1 , . . . , an : wenn ak , 1 ≤ k ≤ n keine Normalform hat, dann hat (f a1 . . . an ) keine NF.
Die Striktheit von f kann zwei verschiedene Ursachen haben:
• Alle Ausdrücke der Form (f a1 . . . an ) haben keine WHNF (f terminiert
nicht), oder
• Jede Auswertung von (f a1 . . . an ) zur WHNF muss irgendwann auch ak
zur WHNF auswerten.
Es gilt: Striktheit ist eine unentscheidbare Eigenschaft von Funktionen, da diese
das Halteproblem für Turingmaschinen umfasst.
Das Wissen über Striktheit von Funktionen kann in der Auswertungsstrategie
ausgenutzt werden.
Hat man mehrere strikte Argumentpositionen von Funktionen, so legt man im
allgemeinen noch eine Reihenfolge von links nach rechts fest, aber auch eine
parallele Auswertung zur WHNF könnte angestoßen werden.
Lemma 5.8.12 Der n-stellige Superkombinator f sei strikt im k-ten Argument.
Wenn (f a1 . . . an )⇓, dann gilt auch ak ⇓.
Beweis. Gibt es a1 , . . . , ak−1 , ak+1 , . . . , an , so dass ak nicht zu WHNF reduziert
wird, dann kann man statt ak auch bot einsetzen. Die Reduktion ändert sich
dadurch nicht. Dies ist aber ein Widerspruch zur Striktheit von f im k-ten
Argument.
2
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
5.9
70
Verzögerte (Lazy) Auswertung
Die Auswertungsidee bei nicht-strikten funktionalen Programmiersprachen kann
man folgendermaßen beschreiben: Es gibt einen Auftrag: “werte aus (zu WHNF,
oder zu Normalform)“. Dieser Auftrag kann Unteraufträge anstoßen. Der oberste Auftrag wird auch dann erfüllt, wenn immer nur die unbedingt erforderlichen
Unteraufträge gestartet werden, d.h wenn nur das allernotwendigste ausgewertet wird. Die Normalordnungsreduktion entspricht dieser Auswertungsstrategie,
wenn zu WHNF ausgewertet werden soll.
Der Auftrag “werte aus Normalform aus“ kann in ähnlicher Weise erfolgen.
Allerdings gibt es verschiedene Möglichkeiten:
1. Die Strategie, den Auftrag “werte zu Normalform aus“, ungeprüft nach
unten weiterzugeben hat manchmal den Nachteil, dass zu viel ausgewertet
wird.
2. Die strikte Auswertung liefert zwar immer eine Normalform, wenn sie terminiert, aber sie kann den Auftrag nicht immer erfüllen, dann manchmal
terminiert diese nicht, obwohl eine Normalform existiert.
3. Ein Strategie, die die Normalform, falls sie existiert, immer findet ist:
(a) Werte zu WHNF aus.
(b) Werte die Komponenten der WHNF ebenfalls zu WHNF aus, usw
rekursiv.
Erinnerung: Ein Ausdruck ist in WHNF, wenn er von folgender Form ist:
1. (c t1 . . . tn ) und c ist ein Konstruktor
2. (f t1 . . . tn ) und f ist ein Superkombinator.
3. λx.s
Wir vergleichen den Ressourcenbedarf von Implementierungen der verschiedenen Reihenfolgen der Auswertung:
Vergleicht man die Reduktion in Anwendungsordnung und Normalordnung auf
Ausdrücken in Baumform, dann stellt man fest: Beim Reduzieren der Ausdrücke
(wie bisher) kann die Reduktion in Normalordnung exponentiell mehr Platz und
Zeit benötigen als die Reduktion in Anwendungsordnung:
Beispiel 5.9.1 Wir berechnen 2n auf eine etwas merkwürdige Weise:
dd n x = (if n == 0 then x else (dd (n - 1) (x + x)))
(dd n 1) ergibt dann 2n .
71
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
Die
Auswertung
in
Anwendungsordnung
sieht
so
aus:
(dd 10 1) →
(if 10 == 0 then 1 else (dd (10 - 1) (1 + 1)))
→
(dd (10 - 1) (1 + 1))
→
(dd 9 2)
→
(if 9 == 0 then 2 else (dd (9 - 1) (2 + 2)))
→
(dd 8 4)
→
(if 8 == 0 then 4 else (dd (8 - 1) (4 + 4)))
...
...
→
1024
Die
Auswertung
in
Normalordnung
sieht
so
aus:
(dd 10 1) → (if 10 == 0 then 1 else (dd (10 - 1) (1 + 1)))
→
(dd (10 - 1) (1 + 1))
→
(if 10 - 1 == 0 then (1 + 1)
else (dd ((10 - 1) - 1) ((1 + 1) + (1 + 1))))
..........................................................................
Man sieht, dass zunächst ein +-Ausdruck mit 2n 1’en aufgebaut wird, der dann
erst addiert wird.
!!! Dies ist aber ein selbst geschaffenes Problem, denn die Exponentialität kam
durch das explizite Kopieren z.B. des Ausdrucks (1 + 1).
Lösung: Man ersetzt den Baum durch einen gerichteten Graphen. D.h. man
verwendet und verwaltet gemeinsame Knoten (node-sharing) bei der Reduktion.
Wir werden sehen, dass danach die Anzahl der Reduktionsschritte in dieser
Auswertung bei Anwendungsordnung und Normalordnung wieder gleich ist .
+
+
+
+
+
1
1
+
+
+
1
+
1
1
1
1
+
1
1
5.9.1
1
Anzahl der Reduktionen in DAG-Darstellung
Wir simulieren die gemeinsamen Knoten durch einen Baum mit Gleichheitsmarkierungen (z.B. eine Zahl). Bei der Beta-Reduktion werden Argumente in der
Simulation kopiert, aber mit gleicher Markierung versehen. Die Beta-Reduktion
wird jeweils analog auf allen gleich markierten Termen durchgeführt (und zählt
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
72
dann nur als ein Schritt). Ein Term der unmittelbar reduziert wird, verliert seine
Markierung auf der obersten Ebene.
Definition 5.9.2 Die Reduktionsstrategie, die Normalordnung verwendet und
gemeinsame Knoten verwendet (shared nodes), nennt man verzögerte Reduktion (lazy reduction). Wir bezeichnen diese Reduktion mit →lz .
Eine Reduktion, die entweder eine Beta-Reduktions oder aus eine CaseReduktions durchführt und sharing beachtet, sei als →s bezeichnet.
Die Anwendungsordnung mit sharing bezeichnen wir als →aos .
In der Implementierung werden dann als gleich markierte Terme auch gemeinsam benutzt, d.h. nur einmal gespeichert.
Lemma 5.9.3 Gleich markierte Positionen liegen stets nebeneinander, d.h. an
unabhängigen Positionen. Sie sind gleich, auch wenn man alle Markierungen
mit beachtet.
Beweis. Das folgt allein schon daraus, dass die als gleich markierten Positionen
identische Ausdrücke sind, die nicht ineinander liegen können. Dass die Terme
gleich sind, folgt daraus, dass sie stets gleich behandelt werden.
2
In den folgenden Lemmas und Aussagen verwenden wir Terme mit sharing.
Lemma 5.9.4 Wenn t →lz t0 und t →s t00 , dann gilt entweder t0 = t00 , oder
t00 →lz t0 , oder es gibt ein t000 , so dass t00 →lz t000 und t0 →s t000 .
{ t CCC
CC
{{
{
CC
{
t
CC
{{
>>>
{
!
{
}
>>
lazy >>
t00
t0 B
>
{
B
lazy {
lazy
0 o_ _ _ _ _ _ 00
B
t
t
{
B!
}{
∃t000
Beweis.
Wir argumentieren in diesem Beweis mit gerichteten Graphen, simuliert mit
Markierungen in Bäumen. Allerdings erfordert ein genauer Beweis eine besser handhabbare Formalisierung. Die hier gegebenen Begründungen sind eher
eine Skizze. Zudem ist diese Methode falsch bei Erweiterung auf nichtdeterministische Kalküle.
i) Sei t ≡ R[f t1 . . . tn ] geschlossen und f t1 . . . tn der Normalordnungs-Redex.
Dann hat (f t1 . . . tn ) keine freien Variablen. Sei f definiert als f x1 . . . xn =
r. Dann wird f t1 . . . tn zu r[t1 /x1 . . . tn /xn ] reduziert. Um alle Reduktionen anzudeuten, schreiben wir: t ≡ C[f t1 . . . tn , . . . , f t1 . . . tn ]. Die lazy
no
Reduktion reduziert alle gleichzeitig. D.h. C[f t1 . . . tn , . . . , f t1 . . . tn ] −→
C[r[t1 /x1 . . . tn /xn ], . . . , r[t1 /x1 . . . tn /xn ]].
lazy
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
73
Wenn t →s t00 eine Reduktion ist, die nur von f t1 . . . tn unabhängige Redexe
reduziert, dann sind die Reduktionen vertauschbar.
Wenn die shared-Reduktion auch in einigen ti gemacht wird, dann gilt: ti → t0i ,
und t000 = C 0 [r[t01 /x1 . . . . . . t0n /xn ], wobei alle Reduktionen “parallel“ in den als
gleich markierten Termen gemacht werden, möglicherweise auch in C. Abhängig
davon, ob xi in r vorkommt oder nicht, gilt einer der beiden im Lemma genannten Fälle.
Wenn die shared-Reduktion oberhalb einer Variante des Ausdrucks f t1 . . . tn
gemacht wird, dann enthält t00 evtl. mehr Kopien dieses Terms, aber der Normalordnungsredex hat sich nicht geändert. Das Diagramm lässt sich schließen
wie in den anderen Fällen.
ii) Wenn der NO-Redex ein case-Ausdruck ist, dann ist die Begründung analog.
2
Mit #LR(t) bezeichnen wir die Anzahl der Lazy-Reduktions-schritte des Terms
t bis zum Erreichen einer WHNF. Mit #AO(t) die Anzahl der Reduktionsschritte in Anwendungsordnung (mit sharing). Wir schreiben #AO(t) = ∞, wenn die
Anwendungsordnung nicht terminiert.
Lemma 5.9.5 Sei t ein Ausdruck. Wenn t →lz t0 und #AO(t) < ∞, dann ist
#AO(t0 ) < #AO(t).
Beweis. Induktion nach #AO(t).W enn#AO(t) = 1, dann gilt sowohl t →lz t0
als auch t →aos t0 und damit ist t0 in WHNF.
Wenn #AO(t) = n > 1, dann betrachte t →aos t00 . Nach Lemma 5.9.4 gibt es
drei Fälle:
Wenn t00 = t0 , dann können wir Induktion verwenden.
Wenn t00 →lz t0 , dann können wir Induktion verwenden und erhalten: #AO(t0 ) <
#AO(t00 ) < #AO(t).
Wenn t00 →lz t000 und t0 →aos t000 , dann gilt mit Induktion #AO(t000 ) < #AO(t00 ),
also auch #AO(t0 ) = #AO(t000 ) + 1 < #AO(t00 ) + 1 = #AO(t).
tC
{{ CCC aos
{
CC
{
CC
{{
{
C!
}{{
0
t B
t00
B aos lazy { {
B
{
B!
}{
000
∃t
lazy
2
Aussage 5.9.6 #LR(t) ≤ #AO(t).
Beweis. Wir können annehmen, dass #AO(t) < ∞. Mit Induktion nach der
Anzahl der Schritte in AO zur WHNF. Wenn #AO(t) = 0, dann ist t in WHNF,
74
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
also ist auch #LR(t) = 0. Sei #AO(t) = n > 0. Dann betrachte t →aos t00 . Sei
t →lz t0 . Es gilt #AO(t00 ) = n − 1.
Wenn t0 = t00 , dann können wir Induktion verwenden.
Wenn t00 →lz t0 , dann können wir ebenfalls Induktion verwenden und erhalten
#LR(t00 ) ≤ n − 1, also #LR(t0 ) ≤ n − 2, somit #LR(t) ≤ n − 1 < #AO(t) = n.
Wenn t00 →lz t000 und t0 →aos t000 , dann gilt wegen Lemma 5.9.5, dass #AO(t000 ) <
#AO(t00 ) = n − 1, somit #AO(t0 ) ≤ n − 1, also mit Induktion #LR(t0 ) ≤ n − 1.
Da t →no t0 , erhalten wir #LR(t) ≤ n.
2
Theoretisch gilt: lazy evaluation (verzögerte Reduktion) ist noch nicht optimal in
der Anzahl der Reduktionen. Der Grund liegt in der Ausführung der Ersetzung:
Der Rumpf eines Superkombinators wird jedesmal kopiert, aber darin könnten
sich auswertbare Ausdrücke befinden, die erst nach dem Kopieren des Rumpfs
ausgewertet werden. Analog werden beim Erzeugen von partiellen Anwendungen
teilweise Reduktionen doppelt ausgeführt, obwohl dies nicht notwendig ist.
Es gibt Untersuchungen zu optimalen Reduktionsstrategien im Lambda-Kalkül.
Diese Strategien können nur teilweise in die Implementierung von funktionalen
Programmiersprachen Eingang finden.
Beispiel 5.9.7 polynom x y = (x * x) + (y * y)
Werte folgenden Ausdruck aus: (\p . (p 2) + (p 3))
(polynom 1)
Man sieht, dass (1 ∗ 1) zweimal ausgewertet wird, obwohl dies eigentlich unnötig
ist.
Die Standardkompilierung achtet nicht auf diese Optimierungsmöglichkeit. Den
Optimierungsschritt, der zum Ziel hat, möglichst viele der reduzierbaren Ausdrücke bereits zur Kompilierzeit auszuwerten, nennt man “partielle Auswertung“. Wie wir gesehen haben, ist die partielle Auswertung korrekt im Sinne
der kontextuellen Äquivalenz.
Es gibt eine Optimierung, die auf ein besseres Kopieren des Funktionsrumpfes
achtet und Ineffizienzen wie oben vermeidet (“fully lazy“).
Beim Kompilieren hat die Anwendungsordnung den Vorteil, dass man zur Kompilierzeit für jede Funktion genau die Reihenfolge der Auswertungen vorhersagen
kann und dass man keinen Test benötigt, ob Ausdrücke ausgewertet sind.
5.9.2
Sequentialität, Parallelität
Betrachtet man die Funktion ||, das logische oder, dann beobachtet man, dass
True || ⊥ zu True auswertet, während ⊥ || True nicht terminiert, obwohl man
eigentlich sagen könnte, dass eigentlich nur True als Wert zurückkommen kann.
Man kann auf die Idee kommen, dies zu verhindern, und zu versuchen, die scheinbar falsche Implementierung von || zu verbessern. Allerdings werden wir zeigen,
dass man keine Funktion definieren kann, die dieses Verhalten zeigt. Wenn man
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
75
dieses Verhalten erzwingen will, dann muss man den ganzen Auswertungsprozess selbst in KFP (Haskell) implementieren und dann den linken und rechten
Term parallel auswerten (d.h. irgendwie abwechselnd).
Aussage 5.9.8 Man kann in KFP (Haskell, Gofer, Lambda-Kalkül,. . . ) keine
arithmetische Funktion ∗ definieren, die
0∗⊥=⊥∗0=0
liefert, aber ansonsten das Produkt der Argumente als Resultat liefert.
Beweis. Es genügt dies für einen Datentyp mit Konstanten 0, 1 zu zeigen und
eine Multiplikationstabelle 0 ∗ 1 = 1 ∗ 0 = 0 ∗ 0 = 0, 1 ∗ 1 = 1. Zusätzlich soll
0 ∗ ⊥ = ⊥ ∗ 0 = 0 aber auch 1 ∗ ⊥ = ⊥ ∗ 1 = ⊥ gelten.
Angenommen, man hat ein f x y in KFP definiert, das dieses leistet. Dann
betrachte die Auswertung (f t1 t2 ).
Die Normalordnung findet eine WHNF von (f t1 t2 ), falls eine existiert. Eine
Eigenschaft der Normalordnung ist: wenn ein Unterausdruck reduziert wird,
dann wird dieser mindestens solange reduziert, bis er selbst in WHNF ist. D.h.,
wenn man f t1 t2 reduziert, dann wird irgendwann zum ersten mal entweder
t1 oder t2 reduziert. Diese Entscheidung hängt aber nicht von der speziellen
Form von t1 oder t2 ab. ObdA sei t1 das erste Argument, das in Normalordnung
reduziert wird. Dann ist f ⊥ t2 = ⊥ , unabhängig vom Inhalt von t2 , denn wenn
es einen anderen Wert hätte, würde die Normalordnung die Kopfnormalform
nicht finden. Damit kann es eine solche Funktion nicht geben.
2
Aussage 5.9.9 Für jeden KFPTS-Superkombinator f gilt:
Entweder hat für jede Anzahl n und alle Argumente ai der Term f a1 . . . an
eine Abstraktion als WHNF (d.h. ist ∼c Y K), oder es gibt eine Anzahl n von
Argumenten, so dass eine der folgenden Aussagen gilt:
• ∀a1 , . . . an : (f a1 . . . an )⇑.
• Es gibt ein Konstruktor c, so dass ∀a1 , . . . an : f a1 . . . an hat eine WHNF
mit c als Top-Konstruktor.
• f ist strikt in mindestens einem Argument.
Beweis. Wir nehmen an, dass f 6∼c Y K. Die Reduktion von f x1 . . . xn kann
folgendes ergeben:
• sie kann nicht terminieren oder einen Typfehler liefern. Dann gilt 5.9.9
• oder es ergibt sich ein Term der Form (c t_1 ... t_m); dann gilt Fall
5.9.9.
• Sie kann einen Term der Form λy.t ergeben. Dann kann man noch ein
Argument hinzufügen. Das Hinzufügen terminiert, denn f ∼c Y K war
ausgeschlossen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
76
• Sie kann einen Term R[xi ] ergeben, wobei R ein Reduktionskontext ist.
Das ist Fall 5.9.9.
2
Beachte, dass der Fall f ∼c Y K bei polymorpher Typisierung nicht auftritt,
da der Typ unendlich groß sein müsste. Die Anzahl n lässt sich bei polymorph
getypten Superkombinatoren am Typ ablesen.
5.10
Church-Zahlen: KFP ist nicht minimal
Das Konstrukt case und Konstruktoren sind nicht unbedingt notwendig, wobei hier gemeint ist, dass man das Verhalten von case und Konstruktoren in
KFP ohne Konstruktoren implementieren kann. Die kontextuelle Äquivalenz
geht dabei möglicherweise verloren.
Als erstes Beispiel können wir Funktionen definieren, die sich wie die Booleschen Funktionen verhalten: Danach die Zahlen, wie A. Church sie definiert hat
Church-Zahlen).
1. man definiert Boolesche Funktionen:
T RU E x y
F ALSE x y
IF x y z
AN D x y
OR x y
N OT x
=
=
=
=
=
=
x
y
(x y z)
x (y T RU E F ALSE) F ALSE
x T RU E y
x F ALSE T RU E
2. Paare und auch Tupel:
P AIR x y z
F ST x
SN D x
= zxy
= x T RU E
= x F ALSE
3. Zahlen (nach A. Church.) Die Darstellung der Zahlen ist: zahl s z =
s (s . . . (s z) . . .)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
ZERO x y
SU CC n s z
P LU S m n
T IM ES m n
EXP T b e
P REDR p
P RED n
M IN U S m n
ZERO? n
EQU AL? m n
=
=
=
=
=
=
=
=
=
=
77
y
s (n s z)
m SU CC n
m (P LU S n) ZERO
e (T IM ES b)(SU CC ZERO)
P AIR (SU CC (F ST p))(F ST p)
SN D (n P REDR (P AIR ZERO ZERO))
n P RED m
n (T RU E F ALSE) T RU E
(AN D (ZERO? (M IN U S m n))
(ZERO? (M IN U S n m)))
(P AIR x y) soll die Paarbildung nachbilden. Beachte, dass ZERO und FALSE
diesselbe Funktion sind.
Übungsaufgabe 5.10.1 Berechne mit obigen Definitionen 1 = 1 und 1 = 2:
Man kann nachprüfen, dass folgendes gilt:
• P RED (SU CC n) = n, wenn n von der Form (SU CC k ZERO) ist.
• P RED ZERO = ZERO, d.h. P RED berechnet für positive Zahlen n die
nächstkleinere Zahl und für die N ull ist das Resultat jeweils N ull.
Als Beispiel berechnen wir: (P RED ZERO) und kürzen ab: T RU E =
T, F ALSE = F, ZERO = Z
(P RED Z) →
→
→
→
=
SN D (Z P REDR (P AIR Z Z))
(Z P REDR (P AIR Z Z)) F
(P AIR Z Z) F
F Z Z
Z
ZERO? ZERO
→ ZERO (T F ) T
→ T
ZERO? (SU CC ZERO) → SU CC Z (T F ) T
→ T F (Z (T F ) T )
→ F
Will man negative Zahlen codieren, so kann man dies durch Zahlen mit Vorzeichen in der folgenden Form durchführen: +n als (P AIR T RU E n) −n als
(P AIR F ALSE n)
Übungsaufgabe 5.10.2 Definition der Rechenoperationen auf diesen ganzen
Zahlen.
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
78
Bemerkung 5.10.3 Was bleibt (nicht) erhalten bei dieser Übersetzung?
Ungetypte Ausdrücke werden teilweise in terminierende übersetzt, da das case nicht exakt übersetzbar ist. Die kontextuelle Ordnung bleibt ebenfalls nicht
erhalten, da Y K nach der Übersetzung zum größten Element wird.
Was erhalten bleibt, ist die Eigenschaft:
no,∗
no,∗
Wenn t −−−→ t0 wobei t0 in WHNF, dann auch τ (t) −−−→ τ (t0 ), wobei τ (t0 ) in
WHNF. D.h. nur erfolgreiche Berechnungen werden korrekt übersetzt.
D.h. diese Übersetzung ist keine volle Übersetzung der gesamten Sprache
KFPTS, sondern eine, die Berechnungen simulieren kann.
Betrachtet man nur die Übersetzung der getypten Ausdrücke, so könnte sich die
Situation etwas verbessern: unklar.
5.10.1
SKI-Kombinatoren
Es gibt eine Übersetzung des Lambda-Kalküls in eine einfache Superkombinatorsprache, bei der ebenfalls die Äquivalenz erhalten bleibt: die sogenannte
SKI-Übersetzung.
Ursprüngliche Motivation war das Finden einer einfacheren Sprache, die mit
wenigen Kombinatoren auskommt, volle Ausdrucksstärke hat, aber in der die
unangenehme Operation der Ersetzung von Variablen durch Terme (in der betaReduktion) durch etwas einfacheres ersetzt ist.
Die Sprache KFP (KFPTS) muss man erst vorbehandeln, d.h. in den Lambdakalkül übersetzen, um danach diese Übersetzung verwenden zu können.
Dazu benötigen wird drei Kombinatoren S, K, und I:
Sxyz
K xy
I x
= (x z) (x z)
= x
= x
Diese Definitionen können offenbar in KFPTS, also auch in KFP durchgeführt
werden. Die Reduktion ist wie in KFPTS. Die Übersetzung von reinen LambdaTermen benötigt die jeweils abstrahierte Variable und eine erweiterte ZwischenDarstellung von Ausdrücken.
[x]E bedeutet: die Variable x soll aus E abstrahiert werden und hat Wirkung
analog zu (λx.E). Dies kann auch verschachtelt sein.
λ-Ausdrücke werden so übersetzt:
λx.E → ([x] E)
Danach wirken die folgenden drei Übersetzungsregeln:
1) [x] (a b) → S ([x] a) ([x] b)
2) [x] x
→ I
3) [x] y
→ K y (y Variable oder Konstante, die von x
verschieden ist. Die Konstanten hier nur:S, K, I)
Funktionale Programmierung I, SS 2003, kontextuelle Gleichheit, Standardisierung, 25. Januar 2007
79
Beispiel 5.10.4 Wir übersetzen
T RU E x y = λx.(λy.x)) in SKI:
[x] ([y] x) → [x] (K x) → S ([x] K)([x] x) → S (K K) I
Die Reduktion von (T RU E a) mittels SKI ergibt:
S (K K) I a → (K K a) (I a) → K a
Man kann jetzt zeigen, dass die Wirkung des Originalausdrucks und des übersetzten Ausdrucks identisch ist.
Die β-Reduktion wird durch die SKI-Kombinatoren implementiert:
(λx.E) t → E[x/t] . Die übersetzte Version leistet das gleiche: ([x]E) t0 hat drei
Möglichkeiten:
1. E ist Applikation. Dann wurde dies mittels S übersetzt:
([x] (a b)) t0 → S ([x] a)([x] b) t0 → ([x] a) t0 )(([x] b) t0 )
2. E ist die Variable x:
([x] x) t0 → I t0 → t0
3. E ist Konstante oder eine Variable 6= x ([x] y) t0 → K y t0 → y
Man kann zeigen, dass α-äquivalente (bis auf Umbenennung gleiche) LambdaAusdrücke in den gleichen SKI-Kombinatorausdruck übersetzt werden, und
dass Namenskonventionen bei der durch SKI simulierten β-Reduktion erhalten
bleiben.
Kontextuelle Gleichheit ist übertragbar.
Was auch noch gilt, ist die Eigenschaft, dass irreduzible Lambda-Ausdrücke in
irreduzible SKI-Ausdrücke übersetzt werden.
Die Anzahl der Reduktionsschritte in beiden Kalkülen ist verschieden, da in
SKI auch die Ersetzung im Term als mehrere Reduktionsschritte gezählt werden.
Die Ausdrücke können sich exponentiell vergrößern bei dieser Übersetzung:
λx1 , . . . , xn , xn+1 .xn+1
→ [x1 ] . . . [xn ]K
→ [x1 ] . . . [xn−1 ](K K)
→ [x1 ] . . . [xn−2 ](S ([xn−1 ]K) ([xn−1 ]K))
→ [x1 ] . . . [xn−2 ](S (K K) (K K))
...
Man sieht dass die Anzahl der Vorkommen von K am Ende mindestens 2n ist
Herunterladen